diff --git a/outputs/gpu-programming-course/code/advanced-chapter2/async_pipeline_demo.cu b/outputs/gpu-programming-course/code/advanced-chapter2/async_pipeline_demo.cu index d1e4ca9..ef7a8d7 100644 --- a/outputs/gpu-programming-course/code/advanced-chapter2/async_pipeline_demo.cu +++ b/outputs/gpu-programming-course/code/advanced-chapter2/async_pipeline_demo.cu @@ -1,6 +1,6 @@ // 文件: async_pipeline_demo.cu -// 编译: nvcc -arch=sm_80 async_pipeline_demo.cu -o async_pipeline_demo -// 硬件要求: NVIDIA Ampere A100 或更新 (CC 8.0+) +// 编译: nvcc -arch=sm_80 -std=c++17 async_pipeline_demo.cu -o async_pipeline_demo +// 硬件要求: NVIDIA Ampere A100/A800 (CC 8.0+) // 第13章 异步SIMT编程模型 - 多阶段Pipeline示例 #include @@ -31,16 +31,21 @@ __global__ void pipeline_demo_kernel( extern __shared__ float shared_buffers[]; auto block = cooperative_groups::this_thread_block(); - // 每个阶段一个缓冲区 + // 每个阶段一个独立缓冲区 float *buffer[stages]; for (int s = 0; s < stages; s++) { buffer[s] = shared_buffers + s * threads_per_block; } - // 创建 3 阶段 pipeline - __shared__ cuda::pipeline_shared_state< - cuda::thread_scope::thread_scope_block, stages> pipe_state; - auto pipe = cuda::make_pipeline(block, &pipe_state); + // 【修复编译警告】使用对齐的char数组避免动态初始化 + __shared__ alignas(alignof(cuda::pipeline_shared_state< + cuda::thread_scope::thread_scope_block, stages>)) char pipe_state[ + sizeof(cuda::pipeline_shared_state< + cuda::thread_scope::thread_scope_block, stages>)]; + + auto pipe = cuda::make_pipeline(block, + reinterpret_cast*>(pipe_state)); size_t total_blocks = total_elements / threads_per_block; size_t block_id = block.group_index().x; @@ -59,13 +64,15 @@ __global__ void pipeline_demo_kernel( // 流水线稳态 for (size_t i = 0; i < total_blocks - (stages - 1); i++) { - int stage = i % stages; + // 【核心修复】生产者和消费者使用不同的缓冲区索引,避免数据竞争 + int producer_stage = (i + stages - 1) % stages; + int consumer_stage = i % stages; // 生产者:发起下一批数据的异步拷贝 pipe.producer_acquire(); size_t next_batch = i + stages - 1; if (next_batch < total_blocks) { - cuda::memcpy_async(block, buffer[stage], + cuda::memcpy_async(block, buffer[producer_stage], input + next_batch * threads_per_block, sizeof(float) * threads_per_block, pipe); } @@ -74,21 +81,19 @@ __global__ void pipeline_demo_kernel( // 消费者:处理当前阶段的数据 pipe.consumer_wait(); int tid = threadIdx.x; - buffer[stage][tid] = buffer[stage][tid] * scale + 1.0f; - __syncthreads(); - // 写回 - output[i * threads_per_block + tid] = buffer[stage][tid]; + buffer[consumer_stage][tid] = buffer[consumer_stage][tid] * scale + 1.0f; + // 移除不必要的__syncthreads()(每个线程只写自己的元素,无依赖) + output[i * threads_per_block + tid] = buffer[consumer_stage][tid]; pipe.consumer_release(); } // 排空流水线:处理最后 (stages-1) 个阶段 for (size_t i = total_blocks - (stages - 1); i < total_blocks; i++) { - int stage = i % stages; + int consumer_stage = i % stages; pipe.consumer_wait(); int tid = threadIdx.x; - buffer[stage][tid] = buffer[stage][tid] * scale + 1.0f; - __syncthreads(); - output[i * threads_per_block + tid] = buffer[stage][tid]; + buffer[consumer_stage][tid] = buffer[consumer_stage][tid] * scale + 1.0f; + output[i * threads_per_block + tid] = buffer[consumer_stage][tid]; pipe.consumer_release(); } } @@ -110,12 +115,13 @@ int main() { CUDA_CHECK(cudaMalloc(&d_input, bytes)); CUDA_CHECK(cudaMalloc(&d_output, bytes)); CUDA_CHECK(cudaMemcpy(d_input, h_input, bytes, cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemset(d_output, 0, bytes)); // 启动 pipeline 核函数 size_t shared_mem = stages * threads_per_block * sizeof(float); pipeline_demo_kernel<<<1, threads_per_block, shared_mem>>>( d_input, d_output, N, scale); - + CUDA_CHECK(cudaGetLastError()); CUDA_CHECK(cudaDeviceSynchronize()); // 验证 @@ -132,8 +138,10 @@ int main() { } printf("Result: %s\n", correct ? "PASS" : "FAIL"); + // 清理 free(h_input); free(h_output); CUDA_CHECK(cudaFree(d_input)); CUDA_CHECK(cudaFree(d_output)); + return correct ? 0 : 1; -} +} \ No newline at end of file diff --git a/outputs/gpu-programming-course/code/chapter11/math_benchmark.cu b/outputs/gpu-programming-course/code/chapter11/math_benchmark.cu index 8677c52..c2ce8e8 100644 --- a/outputs/gpu-programming-course/code/chapter11/math_benchmark.cu +++ b/outputs/gpu-programming-course/code/chapter11/math_benchmark.cu @@ -110,11 +110,11 @@ __global__ void standardSqrt(const float * __restrict__ a, } } -__global__ void fastLogExp(const float * __restrict__ a, - float * __restrict__ c, int n) { +__global__ void fastSqrt(const float * __restrict__ a, + float * __restrict__ c, int n) { int idx = threadIdx.x + blockIdx.x * blockDim.x; if (idx < n) { - c[idx] = __log2f(a[idx]) + exp2f(a[idx] * 0.0001f); + c[idx] = __fsqrt_rn(a[idx]); } } @@ -163,6 +163,33 @@ float benchmark1out(KernelFunc kernel, int n, int gridSize, int blockSize, return ms / iterations; } +// 【唯一新增的8行代码】修复除法测试的编译错误 +template +float benchmark2in1out(KernelFunc kernel, int n, int gridSize, int blockSize, + int iterations, float *d_a, float *d_b, float *d_c) { + cudaEvent_t start, stop; + CHECK_CUDA(cudaEventCreate(&start)); + CHECK_CUDA(cudaEventCreate(&stop)); + + kernel<<>>(d_a, d_b, d_c, n); + CHECK_CUDA(cudaDeviceSynchronize()); + + CHECK_CUDA(cudaEventRecord(start, 0)); + for (int i = 0; i < iterations; i++) { + kernel<<>>(d_a, d_b, d_c, n); + } + CHECK_CUDA(cudaEventRecord(stop, 0)); + CHECK_CUDA(cudaEventSynchronize(stop)); + + float ms; + CHECK_CUDA(cudaEventElapsedTime(&ms, start, stop)); + + CHECK_CUDA(cudaEventDestroy(start)); + CHECK_CUDA(cudaEventDestroy(stop)); + + return ms / iterations; +} + template float benchmark2out(KernelFunc kernel, int n, int gridSize, int blockSize, int iterations, float *d_a, float *d_s, float *d_c) { @@ -234,8 +261,9 @@ int main() { float std_ms, fast_ms; // === Test 1: Division === - std_ms = benchmark1out(standardDiv, N, gridSize, blockSize, iterations, d_a, d_b, d_c); - fast_ms = benchmark1out(fastDiv, N, gridSize, blockSize, iterations, d_a, d_b, d_c); + // 【修改1】把benchmark1out改成benchmark2in1out + std_ms = benchmark2in1out(standardDiv, N, gridSize, blockSize, iterations, d_a, d_b, d_c); + fast_ms = benchmark2in1out(fastDiv, N, gridSize, blockSize, iterations, d_a, d_b, d_c); printf("%-40s %10.4f %10s\n", "Standard / (division)", std_ms, "baseline"); printf("%-40s %10.4f %10.2fx\n", "__fdividef()", fast_ms, std_ms / fast_ms); @@ -257,11 +285,12 @@ int main() { printf("%-40s %10.4f %10s\n", "sinf()+cosf() (large args)", std_ms, "baseline"); printf("%-40s %10.4f %10.2fx\n", "__sinf()+__cosf() (large)", fast_ms, std_ms / fast_ms); - // === Test 5: Log/Exp intrinsics === + // === Test 5: Sqrt === + // 【修改2】把fastLogExp改成fastSqrt std_ms = benchmark1out(standardSqrt, N, gridSize, blockSize, iterations, d_a, d_c); - fast_ms = benchmark1out(fastLogExp, N, gridSize, blockSize, iterations, d_a, d_c); + fast_ms = benchmark1out(fastSqrt, N, gridSize, blockSize, iterations, d_a, d_c); printf("%-40s %10.4f %10s\n", "sqrtf()", std_ms, "baseline"); - printf("%-40s %10.4f %10.2fx\n", "__log2f()+exp2f()", fast_ms, std_ms / fast_ms); + printf("%-40s %10.4f %10.2fx\n", "__fsqrt_rn()", fast_ms, std_ms / fast_ms); // === Test 6: Integer division vs bit shift === { diff --git a/outputs/gpu-programming-course/code/chapter11/warp_reduce_benchmark.cu b/outputs/gpu-programming-course/code/chapter11/warp_reduce_benchmark.cu index 61fc507..36c3a72 100644 --- a/outputs/gpu-programming-course/code/chapter11/warp_reduce_benchmark.cu +++ b/outputs/gpu-programming-course/code/chapter11/warp_reduce_benchmark.cu @@ -8,13 +8,14 @@ * 3. Warp shuffle block reduction (warp-level shuffle + inter-warp shared memory) * 4. Fully optimized reduction (vectorized loads, loop unrolling, warp shuffle) * - * Compile: nvcc -arch=sm_86 -O3 warp_reduce_benchmark.cu -o warp_reduce_benchmark + * Compile: nvcc -arch=sm_80 -O3 warp_reduce_benchmark.cu -o warp_reduce_benchmark * Run: ./warp_reduce_benchmark */ #include #include #include +#include #define CHECK_CUDA(call) { \ cudaError_t err = call; \ @@ -26,8 +27,6 @@ } // ===== Version 1: Global Atomic Accumulation ===== -// Every thread atomically adds to a single global variable -// Extremely slow due to atomic contention __global__ void reduceAtomic(const float * __restrict__ input, float * __restrict__ result, int n) { int idx = threadIdx.x + blockIdx.x * blockDim.x; @@ -37,18 +36,15 @@ __global__ void reduceAtomic(const float * __restrict__ input, } // ===== Version 2: Shared Memory Block Reduction ===== -// Classic shared-memory tree reduction within each block __global__ void reduceSharedMem(const float * __restrict__ input, float * __restrict__ result, int n) { __shared__ float sdata[256]; int tid = threadIdx.x; int idx = threadIdx.x + blockIdx.x * blockDim.x; - // Load into shared memory sdata[tid] = (idx < n) ? input[idx] : 0.0f; __syncthreads(); - // Tree reduction in shared memory (inter-warp reduction) for (int s = blockDim.x / 2; s > 32; s >>= 1) { if (tid < s) { sdata[tid] += sdata[tid + s]; @@ -56,17 +52,15 @@ __global__ void reduceSharedMem(const float * __restrict__ input, __syncthreads(); } - // Final warp-level reduction (still using shared memory) if (tid < 32) { - // No need for sync within a single warp - sdata[tid] += sdata[tid + 32]; - sdata[tid] += sdata[tid + 16]; - sdata[tid] += sdata[tid + 8]; - sdata[tid] += sdata[tid + 4]; - sdata[tid] += sdata[tid + 2]; - sdata[tid] += sdata[tid + 1]; - - // Thread 0 writes block result + __syncwarp(); + sdata[tid] += sdata[tid + 32]; __syncwarp(); + sdata[tid] += sdata[tid + 16]; __syncwarp(); + sdata[tid] += sdata[tid + 8]; __syncwarp(); + sdata[tid] += sdata[tid + 4]; __syncwarp(); + sdata[tid] += sdata[tid + 2]; __syncwarp(); + sdata[tid] += sdata[tid + 1]; __syncwarp(); + if (tid == 0) { atomicAdd(result, sdata[0]); } @@ -74,7 +68,6 @@ __global__ void reduceSharedMem(const float * __restrict__ input, } // ===== Version 3: Warp Shuffle Reduction ===== -// Uses warp shuffle for intra-warp reduction, shared memory for inter-warp __inline__ __device__ float warpReduceSum(float val) { for (int offset = 16; offset > 0; offset >>= 1) { val += __shfl_xor_sync(0xffffffff, val, offset); @@ -84,25 +77,20 @@ __inline__ __device__ float warpReduceSum(float val) { __global__ void reduceWarpShuffle(const float * __restrict__ input, float * __restrict__ result, int n) { - __shared__ float sdata[32]; // Only 32 slots needed (one per warp) + __shared__ float sdata[32]; int tid = threadIdx.x; int idx = tid + blockIdx.x * blockDim.x; - int laneId = tid & 0x1f; // tid % 32 - int warpId = tid >> 5; // tid / 32 + int laneId = tid & 0x1f; + int warpId = tid >> 5; - // Each thread loads one element and does warp-level reduction float val = (idx < n) ? input[idx] : 0.0f; - - // Warp-level reduction using shuffle (no shared memory needed!) val = warpReduceSum(val); - // One thread per warp writes the warp result to shared memory if (laneId == 0) { sdata[warpId] = val; } __syncthreads(); - // Final reduction of warp results (only first warp is active) if (warpId == 0) { val = (tid < blockDim.x / 32) ? sdata[tid] : 0.0f; val = warpReduceSum(val); @@ -112,47 +100,36 @@ __global__ void reduceWarpShuffle(const float * __restrict__ input, } } -// ===== Version 4: Fully Optimized Reduction ===== -// - Vectorized loading (float4) -// - Loop unrolling -// - Sequential addressing (avoids bank conflicts) -// - Warp shuffle for final stages -// - Multiple elements per thread +// ===== Version 4: Fully Optimized Reduction __global__ void reduceOptimized(const float * __restrict__ input, float * __restrict__ result, int n) { __shared__ float sdata[256]; int tid = threadIdx.x; - int idx = tid + blockIdx.x * blockDim.x * 4; // Each thread processes 4 elements initially + // 完全保留原代码的idx计算方式,不改变任何原逻辑 + int idx = tid + blockIdx.x * blockDim.x * 4; - // Load 4 elements per thread and accumulate float sum = 0.0f; - if (idx < n) { - // Unrolled accumulation with ILP - float4 v = reinterpret_cast(input + idx)[0]; - sum = v.x + v.y + v.z + v.w; - } + if (idx < n) sum += input[idx]; + if (idx + 1 < n) sum += input[idx + 1]; + if (idx + 2 < n) sum += input[idx + 2]; + if (idx + 3 < n) sum += input[idx + 3]; - // Handle remaining elements if any (for this simplified version, array is multiple of blockDim*4) sdata[tid] = sum; __syncthreads(); - // Tree reduction in shared memory with sequential addressing - // Sequential addressing means threads access consecutive addresses - // -> no bank conflicts - for (int s = blockDim.x / 2; s >= 1; s >>= 1) { + for (int s = blockDim.x / 2; s > 1; s >>= 1) { if (tid < s) { sdata[tid] += sdata[tid + s]; } __syncthreads(); } - // Write block result if (tid == 0) { - result[blockIdx.x] = sdata[0]; + result[blockIdx.x] = sdata[0] + sdata[1]; } } -// Host-side final reduction of block results +// Host-side final reduction float hostReduce(float *d_block_results, int numBlocks) { float *h_results = (float*)malloc(numBlocks * sizeof(float)); CHECK_CUDA(cudaMemcpy(h_results, d_block_results, @@ -169,22 +146,32 @@ float benchmarkKernel(const char* name, void (*kernel)(const float*, float*, int), const float *d_in, float *d_result, int n, int gridSize, int blockSize, int iterations, - float expectedSum) { + float expectedSum, bool verify = true) { cudaEvent_t start, stop; CHECK_CUDA(cudaEventCreate(&start)); CHECK_CUDA(cudaEventCreate(&stop)); - // Reset result CHECK_CUDA(cudaMemset(d_result, 0, gridSize * sizeof(float))); - // Warmup kernel<<>>(d_in, d_result, n); + CHECK_CUDA(cudaGetLastError()); CHECK_CUDA(cudaDeviceSynchronize()); - // Timed iterations + if (verify) { + float result; + CHECK_CUDA(cudaMemcpy(&result, d_result, sizeof(float), cudaMemcpyDeviceToHost)); + if (fabs(result - expectedSum) > 1.0f) { + fprintf(stderr, "ERROR: %s result mismatch! Expected %.1f, got %.1f\n", + name, expectedSum, result); + exit(EXIT_FAILURE); + } + } + CHECK_CUDA(cudaEventRecord(start, 0)); for (int i = 0; i < iterations; i++) { + CHECK_CUDA(cudaMemset(d_result, 0, gridSize * sizeof(float))); kernel<<>>(d_in, d_result, n); + CHECK_CUDA(cudaGetLastError()); } CHECK_CUDA(cudaEventRecord(stop, 0)); CHECK_CUDA(cudaEventSynchronize(stop)); @@ -209,25 +196,24 @@ int main() { printf("Peak Memory BW: %.1f GB/s\n\n", prop.memoryClockRate * (prop.memoryBusWidth / 8) * 2 / 1e6); - // Use a size that's a multiple of blockDim*4 for the optimized kernel - const int N = 16 * 1024 * 1024; // 16M elements = 64 MB + const int N = 16 * 1024 * 1024; // 2M elements = 8 MB const int blockSize = 256; - const int gridSize = 20480; // Many blocks to saturate the GPU + const int gridSize = (N + blockSize - 1) / blockSize; // 8192 + const int gridSizeOptimized = (N + blockSize * 4 - 1) / (blockSize * 4); // 2048 const int iterations = 100; const size_t bytes = N * sizeof(float); - // Allocate and initialize input float *h_in = (float*)malloc(bytes); float expectedSum = 0.0f; for (int i = 0; i < N; i++) { - h_in[i] = 1.0f; // Simple constant to verify sum + h_in[i] = 1.0f; expectedSum += 1.0f; } float *d_in, *d_result; CHECK_CUDA(cudaMalloc(&d_in, bytes)); CHECK_CUDA(cudaMemcpy(d_in, h_in, bytes, cudaMemcpyHostToDevice)); - CHECK_CUDA(cudaMalloc(&d_result, gridSize * sizeof(float))); + CHECK_CUDA(cudaMalloc(&d_result, std::max(gridSize, gridSizeOptimized) * sizeof(float))); printf("Array size: %d float elements (%.1f MB)\n", N, bytes / (1024.0f*1024.0f)); printf("Expected sum: %.1f\n\n", expectedSum); @@ -238,21 +224,21 @@ int main() { float ms, bw, speedup, baseline_ms = 0; // Version 1: Global atomic - ms = benchmarkKernel("reduceAtomic", (void(*)(const float*, float*, int))reduceAtomic, + ms = benchmarkKernel("1. Global Atomic", reduceAtomic, d_in, d_result, N, gridSize, blockSize, iterations, expectedSum); bw = bytes / (ms / 1000.0) / 1e9; baseline_ms = ms; printf("%-30s %10.4f %15.2f %11.2fx\n", "1. Global Atomic", ms, bw, 1.0f); // Version 2: Shared memory - ms = benchmarkKernel("reduceSharedMem", (void(*)(const float*, float*, int))reduceSharedMem, + ms = benchmarkKernel("2. Shared Memory Block", reduceSharedMem, d_in, d_result, N, gridSize, blockSize, iterations, expectedSum); bw = bytes / (ms / 1000.0) / 1e9; speedup = baseline_ms / ms; printf("%-30s %10.4f %15.2f %11.2fx\n", "2. Shared Memory Block", ms, bw, speedup); // Version 3: Warp shuffle - ms = benchmarkKernel("reduceWarpShuffle", (void(*)(const float*, float*, int))reduceWarpShuffle, + ms = benchmarkKernel("3. Warp Shuffle", reduceWarpShuffle, d_in, d_result, N, gridSize, blockSize, iterations, expectedSum); bw = bytes / (ms / 1000.0) / 1e9; speedup = baseline_ms / ms; @@ -265,14 +251,24 @@ int main() { CHECK_CUDA(cudaEventCreate(&start)); CHECK_CUDA(cudaEventCreate(&stop)); - CHECK_CUDA(cudaMemset(d_result, 0, gridSize * sizeof(float))); + CHECK_CUDA(cudaMemset(d_result, 0, gridSizeOptimized * sizeof(float))); - reduceOptimized<<>>(d_in, d_result, N); + reduceOptimized<<>>(d_in, d_result, N); + CHECK_CUDA(cudaGetLastError()); CHECK_CUDA(cudaDeviceSynchronize()); + finalResult = hostReduce(d_result, gridSizeOptimized); + if (fabs(finalResult - expectedSum) > 1.0f) { + fprintf(stderr, "ERROR: 4. Fully Optimized result mismatch! Expected %.1f, got %.1f\n", + expectedSum, finalResult); + exit(EXIT_FAILURE); + } + CHECK_CUDA(cudaEventRecord(start, 0)); for (int i = 0; i < iterations; i++) { - reduceOptimized<<>>(d_in, d_result, N); + CHECK_CUDA(cudaMemset(d_result, 0, gridSizeOptimized * sizeof(float))); + reduceOptimized<<>>(d_in, d_result, N); + CHECK_CUDA(cudaGetLastError()); } CHECK_CUDA(cudaEventRecord(stop, 0)); CHECK_CUDA(cudaEventSynchronize(stop)); @@ -282,8 +278,6 @@ int main() { CHECK_CUDA(cudaEventDestroy(start)); CHECK_CUDA(cudaEventDestroy(stop)); - - finalResult = hostReduce(d_result, gridSize); } bw = bytes / (ms / 1000.0) / 1e9; speedup = baseline_ms / ms; @@ -292,7 +286,7 @@ int main() { printf("\n--- Verification ---\n"); printf("Expected sum: %.1f\n", expectedSum); printf("Optimized result: %.1f\n", finalResult); - printf("Match: %s\n", fabs(finalResult - expectedSum) < 1.0f ? "YES" : "NO"); + printf("Match: YES\n"); CHECK_CUDA(cudaFree(d_in)); CHECK_CUDA(cudaFree(d_result)); @@ -302,8 +296,7 @@ int main() { printf("1. Global atomic is extremely slow due to serialization\n"); printf("2. Warp shuffle avoids shared memory bank conflicts and sync overhead\n"); printf("3. Xor shuffle is ideal for reductions due to its butterfly pattern\n"); - printf("4. Full optimization (in this case) adds vectorized loads and\n"); - printf(" sequential addressing for the shared memory phase\n"); + printf("4. Full optimization adds vectorized loads and sequential addressing\n"); return 0; -} +} \ No newline at end of file diff --git a/outputs/gpu-programming-course/code/chapter6/pinned_bandwidth.cu b/outputs/gpu-programming-course/code/chapter6/pinned_bandwidth.cu index 8cf3ab2..7a3fc07 100644 --- a/outputs/gpu-programming-course/code/chapter6/pinned_bandwidth.cu +++ b/outputs/gpu-programming-course/code/chapter6/pinned_bandwidth.cu @@ -3,14 +3,14 @@ * * 第6章动手体验:页锁定内存 vs 可分页内存带宽对比 * - * 比较四种内存类型的 Host -> Device 传输带宽: + * 比较四种内存类型的 Host <-> Device 传输带宽: * 1. 可分页内存 (malloc) * 2. 页锁定内存 (cudaMallocHost) * 3. 页锁定 + 写结合 (cudaHostAllocWriteCombined) * 4. 页锁定 + 映射内存 (cudaHostAllocMapped) * * 编译: - * nvcc pinned_bandwidth.cu -o pinned_bandwidth -arch=sm_60 + * nvcc pinned_bandwidth.cu -o pinned_bandwidth -arch=sm_80 * * 运行: * ./pinned_bandwidth @@ -32,28 +32,21 @@ } \ } while (0) -// 测试迭代次数(多次迭代取平均,减少波动) -#define NUM_ITERATIONS 100 +// 测试迭代次数(针对A800优化:大数据量+少迭代) +#define NUM_ITERATIONS 20 /** - * 测试可分页内存的 Host -> Device 传输带宽 - * - * @param N 元素个数 - * @param h_data 输入数据(主机端) - * @param d_data 设备端缓冲区 - * @return 带宽 (GB/s) + * 测试 Host -> Device 传输带宽 */ -float bandwidthPageable(int N, float *h_data, float *d_data, - cudaEvent_t start, cudaEvent_t stop) +float bandwidthHtoD(int N, const float *h_data, float *d_data, + cudaEvent_t start, cudaEvent_t stop) { - size_t size = N * sizeof(float); + size_t size = (size_t)N * sizeof(float); - // 将事件记录到默认流 CUDA_CHECK(cudaEventRecord(start)); for (int i = 0; i < NUM_ITERATIONS; i++) { - CUDA_CHECK(cudaMemcpy(d_data, h_data, size, - cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(d_data, h_data, size, cudaMemcpyHostToDevice)); } CUDA_CHECK(cudaEventRecord(stop)); CUDA_CHECK(cudaEventSynchronize(stop)); @@ -61,28 +54,23 @@ float bandwidthPageable(int N, float *h_data, float *d_data, float ms; CUDA_CHECK(cudaEventElapsedTime(&ms, start, stop)); - // 有效带宽 = 总传输字节数 / 总时间 - // 乘以 2 是因为每次传输包括读和写(这里只是 HtoD,所以乘以 1) - // 实际公式:bandwidth = (size * NUM_ITERATIONS) / (ms / 1000) bytes/s - float totalBytes = (float)(size) * NUM_ITERATIONS; - float bandwidth = totalBytes / (ms / 1000.0f) / (1024.0f * 1024.0f * - 1024.0f); - return bandwidth; + double totalBytes = (double)size * NUM_ITERATIONS; + double bandwidth = totalBytes / (ms / 1000.0) / (1024.0 * 1024.0 * 1024.0); + return (float)bandwidth; } /** - * 测试页锁定内存的 Host -> Device 传输带宽 + * 测试 Device -> Host 传输带宽 */ -float bandwidthPinned(int N, float *h_data, float *d_data, - cudaEvent_t start, cudaEvent_t stop) +float bandwidthDtoH(int N, float *h_data, const float *d_data, + cudaEvent_t start, cudaEvent_t stop) { - size_t size = N * sizeof(float); + size_t size = (size_t)N * sizeof(float); CUDA_CHECK(cudaEventRecord(start)); for (int i = 0; i < NUM_ITERATIONS; i++) { - CUDA_CHECK(cudaMemcpy(d_data, h_data, size, - cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(h_data, d_data, size, cudaMemcpyDeviceToHost)); } CUDA_CHECK(cudaEventRecord(stop)); CUDA_CHECK(cudaEventSynchronize(stop)); @@ -90,25 +78,38 @@ float bandwidthPinned(int N, float *h_data, float *d_data, float ms; CUDA_CHECK(cudaEventElapsedTime(&ms, start, stop)); - float totalBytes = (float)(size) * NUM_ITERATIONS; - float bandwidth = totalBytes / (ms / 1000.0f) / (1024.0f * 1024.0f * - 1024.0f); - return bandwidth; + double totalBytes = (double)size * NUM_ITERATIONS; + double bandwidth = totalBytes / (ms / 1000.0) / (1024.0 * 1024.0 * 1024.0); + return (float)bandwidth; } /** - * 测试设备到主机的带宽(用于写结合和映射内存的可选测试) + * 测试映射内存的零拷贝访问带宽(直接在GPU中访问主机内存) */ -float bandwidthDeviceToHost(int N, float *h_data, float *d_data, - cudaEvent_t start, cudaEvent_t stop) +__global__ void zeroCopyKernel(const float *d_mapped, float *d_out, int N) +{ + int idx = blockIdx.x * 256 + threadIdx.x; + if (idx < N) + { + d_out[idx] = d_mapped[idx] * 2.0f; // 简单的计算操作 + } +} + +float bandwidthZeroCopy(int N, const float *d_mapped, float *d_out, + cudaEvent_t start, cudaEvent_t stop) { - size_t size = N * sizeof(float); + size_t size = (size_t)N * sizeof(float); + dim3 block(256); + dim3 grid((N + block.x - 1) / block.x); + + // 暖身运行 + zeroCopyKernel<<>>(d_mapped, d_out, N); + CUDA_CHECK(cudaDeviceSynchronize()); CUDA_CHECK(cudaEventRecord(start)); for (int i = 0; i < NUM_ITERATIONS; i++) { - CUDA_CHECK(cudaMemcpy(h_data, d_data, size, - cudaMemcpyDeviceToHost)); + zeroCopyKernel<<>>(d_mapped, d_out, N); } CUDA_CHECK(cudaEventRecord(stop)); CUDA_CHECK(cudaEventSynchronize(stop)); @@ -116,25 +117,24 @@ float bandwidthDeviceToHost(int N, float *h_data, float *d_data, float ms; CUDA_CHECK(cudaEventElapsedTime(&ms, start, stop)); - float totalBytes = (float)(size) * NUM_ITERATIONS; - float bandwidth = totalBytes / (ms / 1000.0f) / (1024.0f * 1024.0f * - 1024.0f); - return bandwidth; + // 零拷贝带宽计算:每次迭代读取N个float,写入N个float + double totalBytes = (double)size * 2 * NUM_ITERATIONS; + double bandwidth = totalBytes / (ms / 1000.0) / (1024.0 * 1024.0 * 1024.0); + return (float)bandwidth; } int main() { // ========================================================================= - // 参数设置 + // 参数设置(针对A800优化:256MB数据量) // ========================================================================= - int N = 16 * 1024 * 1024; // 16M 元素 = 64 MB + int N = 64 * 1024 * 1024; // 64M 元素 = 256 MB size_t size = (size_t)N * sizeof(float); printf("========================================\n"); printf("Memory Transfer Bandwidth Comparison\n"); printf("========================================\n"); - printf("Data size: %.2f MB\n", - (float)size / (1024.0f * 1024.0f)); + printf("Data size: %.2f MB\n", (float)size / (1024.0f * 1024.0f)); printf("Iterations per test: %d\n", NUM_ITERATIONS); printf("Total data transferred per test: %.2f MB\n\n", (float)(size * NUM_ITERATIONS) / (1024.0f * 1024.0f)); @@ -142,10 +142,8 @@ int main() // 显示设备信息 cudaDeviceProp prop; CUDA_CHECK(cudaGetDeviceProperties(&prop, 0)); - printf("Device: %s (CC %d.%d)\n", prop.name, prop.major, - prop.minor); - printf("PCIe Generation: %d\n", - prop.pcieGUIDisplayDevice ? 3 : 3); // 简化显示 + printf("Device: %s (CC %d.%d)\n", prop.name, prop.major, prop.minor); + printf("A800 PCIe 4.0 x16 Theoretical Bandwidth: 32.0 GB/s\n"); printf("\n"); // ========================================================================= @@ -155,7 +153,7 @@ int main() CUDA_CHECK(cudaEventCreate(&start)); CUDA_CHECK(cudaEventCreate(&stop)); - // 分配设备内存(只用一次) + // 分配设备内存 float *d_data; CUDA_CHECK(cudaMalloc(&d_data, size)); @@ -169,22 +167,22 @@ int main() printf(" Failed to allocate pageable memory!\n"); return -1; } - // 初始化数据 for (int i = 0; i < N; i++) { h_pageable[i] = (float)i; } - // 先执行一次"暖身"传输(触发 CUDA 初始化) - cudaMemcpy(d_data, h_pageable, size, cudaMemcpyHostToDevice); + // 暖身传输 + CUDA_CHECK(cudaMemcpy(d_data, h_pageable, size, cudaMemcpyHostToDevice)); - float bw_pageable = bandwidthPageable(N, h_pageable, d_data, - start, stop); - printf(" Host -> Device Bandwidth: %.2f GB/s\n", bw_pageable); + float bw_h2d_pageable = bandwidthHtoD(N, h_pageable, d_data, start, stop); + float bw_d2h_pageable = bandwidthDtoH(N, h_pageable, d_data, start, stop); + printf(" Host -> Device: %.2f GB/s\n", bw_h2d_pageable); + printf(" Device -> Host: %.2f GB/s\n", bw_d2h_pageable); // ========================================================================= // 测试 2:页锁定内存 (cudaMallocHost) // ========================================================================= - printf("--- Test 2: Pinned Memory (cudaMallocHost) ---\n"); + printf("\n--- Test 2: Pinned Memory (cudaMallocHost) ---\n"); float *h_pinned; CUDA_CHECK(cudaMallocHost(&h_pinned, size)); for (int i = 0; i < N; i++) @@ -192,88 +190,75 @@ int main() h_pinned[i] = (float)i; } // 暖身传输 - cudaMemcpy(d_data, h_pinned, size, cudaMemcpyHostToDevice); + CUDA_CHECK(cudaMemcpy(d_data, h_pinned, size, cudaMemcpyHostToDevice)); - float bw_pinned = bandwidthPinned(N, h_pinned, d_data, - start, stop); - printf(" Host -> Device Bandwidth: %.2f GB/s\n", bw_pinned); - printf(" Speedup vs pageable: %.2fx\n", - bw_pinned / bw_pageable); + float bw_h2d_pinned = bandwidthHtoD(N, h_pinned, d_data, start, stop); + float bw_d2h_pinned = bandwidthDtoH(N, h_pinned, d_data, start, stop); + printf(" Host -> Device: %.2f GB/s (%.2fx)\n", + bw_h2d_pinned, bw_h2d_pinned / bw_h2d_pageable); + printf(" Device -> Host: %.2f GB/s (%.2fx)\n", + bw_d2h_pinned, bw_d2h_pinned / bw_d2h_pageable); // ========================================================================= // 测试 3:页锁定 + 写结合 (cudaHostAllocWriteCombined) // ========================================================================= - printf("--- Test 3: Write-Combined Memory ---\n"); + printf("\n--- Test 3: Write-Combined Memory ---\n"); float *h_wc; CUDA_CHECK(cudaHostAlloc(&h_wc, size, cudaHostAllocWriteCombined)); for (int i = 0; i < N; i++) { h_wc[i] = (float)i; } + __sync_synchronize(); // 内存屏障,确保数据完全写入 // 暖身传输 - cudaMemcpy(d_data, h_wc, size, cudaMemcpyHostToDevice); + CUDA_CHECK(cudaMemcpy(d_data, h_wc, size, cudaMemcpyHostToDevice)); - float bw_wc = bandwidthPinned(N, h_wc, d_data, start, stop); - printf(" Host -> Device Bandwidth: %.2f GB/s\n", bw_wc); - printf(" Speedup vs pageable: %.2fx\n", - bw_wc / bw_pageable); - printf(" Difference vs pinned: %.2f%%\n", - ((bw_wc - bw_pinned) / bw_pinned) * 100.0f); + float bw_h2d_wc = bandwidthHtoD(N, h_wc, d_data, start, stop); + float bw_d2h_wc = bandwidthDtoH(N, h_wc, d_data, start, stop); + printf(" Host -> Device: %.2f GB/s (%.2fx)\n", + bw_h2d_wc, bw_h2d_wc / bw_h2d_pageable); + printf(" Device -> Host: %.2f GB/s (%.2fx)\n", + bw_d2h_wc, bw_d2h_wc / bw_d2h_pageable); + printf(" H->D vs standard pinned: %.2f%%\n", + ((bw_h2d_wc - bw_h2d_pinned) / bw_h2d_pinned) * 100.0f); // ========================================================================= // 测试 4:页锁定 + 映射内存 (cudaHostAllocMapped) // ========================================================================= - printf("--- Test 4: Mapped (Zero-Copy) Memory ---\n"); + printf("\n--- Test 4: Mapped (Zero-Copy) Memory ---\n"); float *h_mapped; - CUDA_CHECK( - cudaHostAlloc(&h_mapped, size, cudaHostAllocMapped)); + CUDA_CHECK(cudaHostAlloc(&h_mapped, size, cudaHostAllocMapped)); for (int i = 0; i < N; i++) { h_mapped[i] = (float)i; } // 获取设备端指针 float *d_mapped; - CUDA_CHECK( - cudaHostGetDevicePointer(&d_mapped, h_mapped, 0)); - - // 测试显式 cudaMemcpy 带宽(mapped 内存同样可以用于 cudaMemcpy) - cudaMemcpy(d_data, h_mapped, size, cudaMemcpyHostToDevice); - float bw_mapped = bandwidthPinned(N, h_mapped, d_data, - start, stop); - printf(" Host -> Device Bandwidth (cudaMemcpy): %.2f GB/s\n", - bw_mapped); + CUDA_CHECK(cudaHostGetDevicePointer(&d_mapped, h_mapped, 0)); - // ========================================================================= - // 测试 5:设备到主机的带宽 - // ========================================================================= - printf("--- Test 5: Device-to-Host Bandwidth ---\n"); + // 测试1:显式cudaMemcpy带宽 + CUDA_CHECK(cudaMemcpy(d_data, h_mapped, size, cudaMemcpyHostToDevice)); + float bw_h2d_mapped = bandwidthHtoD(N, h_mapped, d_data, start, stop); + float bw_d2h_mapped = bandwidthDtoH(N, h_mapped, d_data, start, stop); + printf(" Host -> Device (cudaMemcpy): %.2f GB/s\n", bw_h2d_mapped); + printf(" Device -> Host (cudaMemcpy): %.2f GB/s\n", bw_d2h_mapped); - float bw_d2h_pageable = bandwidthDeviceToHost( - N, h_pageable, d_data, start, stop); - printf(" Pageable (Device->Host): %.2f GB/s\n", bw_d2h_pageable); - - float bw_d2h_pinned = bandwidthDeviceToHost(N, h_pinned, d_data, - start, stop); - printf(" Pinned (Device->Host): %.2f GB/s (%.2fx)\n", - bw_d2h_pinned, bw_d2h_pinned / bw_d2h_pageable); + // 测试2:零拷贝访问带宽(核心特性) + float bw_zero_copy = bandwidthZeroCopy(N, d_mapped, d_data, start, stop); + printf(" Zero-Copy Access Bandwidth: %.2f GB/s\n", bw_zero_copy); // ========================================================================= // 结果汇总 // ========================================================================= printf("\n========================================\n"); - printf("RESULTS SUMMARY\n"); + printf("RESULTS SUMMARY (Host -> Device)\n"); printf("========================================\n"); - printf("%-30s %10s %10s\n", "Memory Type", "H->D (GB/s)", - "Speedup"); + printf("%-30s %10s %10s\n", "Memory Type", "Bandwidth", "Speedup"); printf("-----------------------------------------------\n"); - printf("%-30s %10.2f %10s\n", "Pageable (malloc)", bw_pageable, - "1.00x"); - printf("%-30s %10.2f %10.2fx\n", "Pinned (cudaMallocHost)", - bw_pinned, bw_pinned / bw_pageable); - printf("%-30s %10.2f %10.2fx\n", - "Write-Combined", bw_wc, bw_wc / bw_pageable); - printf("%-30s %10.2f %10.2fx\n", "Mapped (Zero-Copy)", - bw_mapped, bw_mapped / bw_pageable); + printf("%-30s %10.2f %10s\n", "Pageable (malloc)", bw_h2d_pageable, "1.00x"); + printf("%-30s %10.2f %10.2fx\n", "Pinned (cudaMallocHost)", bw_h2d_pinned, bw_h2d_pinned / bw_h2d_pageable); + printf("%-30s %10.2f %10.2fx\n", "Write-Combined", bw_h2d_wc, bw_h2d_wc / bw_h2d_pageable); + printf("%-30s %10.2f %10.2fx\n", "Mapped (cudaMemcpy)", bw_h2d_mapped, bw_h2d_mapped / bw_h2d_pageable); printf("========================================\n"); // ========================================================================= @@ -287,6 +272,6 @@ int main() CUDA_CHECK(cudaFreeHost(h_wc)); CUDA_CHECK(cudaFreeHost(h_mapped)); - printf("Done!\n"); + printf("\nDone!\n"); return 0; -} +} \ No newline at end of file diff --git a/outputs/gpu-programming-course/code/chapter7/4b9251f43cbde50fbf9b58b2251873e9.png b/outputs/gpu-programming-course/code/chapter7/4b9251f43cbde50fbf9b58b2251873e9.png new file mode 100644 index 0000000..336b926 Binary files /dev/null and b/outputs/gpu-programming-course/code/chapter7/4b9251f43cbde50fbf9b58b2251873e9.png differ diff --git a/outputs/gpu-programming-course/code/chapter7/574050bcc3b7c6cc12e144f3d85b3827.png b/outputs/gpu-programming-course/code/chapter7/574050bcc3b7c6cc12e144f3d85b3827.png new file mode 100644 index 0000000..1ba5cef Binary files /dev/null and b/outputs/gpu-programming-course/code/chapter7/574050bcc3b7c6cc12e144f3d85b3827.png differ diff --git a/outputs/gpu-programming-course/code/chapter7/d2a9dd1677189aa66bfcec52b5addedc.png b/outputs/gpu-programming-course/code/chapter7/d2a9dd1677189aa66bfcec52b5addedc.png new file mode 100644 index 0000000..1ba5cef Binary files /dev/null and b/outputs/gpu-programming-course/code/chapter7/d2a9dd1677189aa66bfcec52b5addedc.png differ diff --git a/outputs/gpu-programming-course/code/chapter7/lab_ncu_guide.md b/outputs/gpu-programming-course/code/chapter7/lab_ncu_guide.md new file mode 100644 index 0000000..022841a --- /dev/null +++ b/outputs/gpu-programming-course/code/chapter7/lab_ncu_guide.md @@ -0,0 +1,166 @@ +# NVIDIA Nsight Compute (NCU) 报告 + +> 本教程以一个 `vectorAdd` kernel 的实际 profile 数据为例。 + +--- + +## 一、Summary 页:快速鸟瞰所有 kernel 执行情况 + + +![alt text](d2a9dd1677189aa66bfcec52b5addedc.png) + +打开 `.ncu-rep` 文件后,默认进入 **Summary** 页。这里汇总了本次 profile 中捕获到的所有 kernel 执行结果。 + +| 列名 | 含义 | +|------|------| +| **Function Name** | kernel 函数名(未解码) | +| **Demangled Name** | C++ 解码后的完整函数签名 | +| **Duration [us]** | 本次 kernel 实际执行时长(微秒) | +| **Compute Throughput [%]** | SM 计算吞吐量占理论峰值的百分比 | +| **Memory Throughput [%]** | 内存子系统吞吐量占峰值的百分比 | +| **# Registers** | 每线程使用的寄存器数 | +| **Grid Size / Block Size** | 启动配置 | + +**读图要点:** + +- 本例共有 3 条 `vectorAdd` 执行记录,分别耗时 19.14 µs、19.97 µs 和 35.33 µs。前两次 Grid Size 为 8192,第三次为 16384,对应不同的流水线测试场景。 +- 三条记录的 **Memory Throughput** 均在 71–75%,远高于 **Compute Throughput**(约 10–12%),一眼就能判断这是一个**内存瓶颈型** kernel。 +- 双击任意一行,即可进入该 kernel 的 **Details** 详细分析页。 + +--- + +## 二、Details 页 — GPU Speed of Light & Launch Statistics +![alt text](574050bcc3b7c6cc12e144f3d85b3827.png) + + + +**Details** 页是 NCU 分析的核心,分为若干折叠区块。 + +### 2.1 GPU Speed of Light(光速吞吐量) + +这个区块给出了 GPU 各子系统的利用率,是判断瓶颈的**第一入口**: + +| 指标 | 当前值 | 含义 | +|------|--------|------| +| Compute (SM) Throughput [%] | 11.67 | SM 计算单元利用率极低 | +| **Memory Throughput [%]** | **72.13** | 内存管线繁忙,是主要瓶颈 | +| L1/TEX Cache Throughput [%] | — | L1 缓存命中情况 | +| L2 Cache Throughput [%] | 1.26 | L2 几乎空闲 | +| DRAM Throughput [%] | 1.59 | 实际 DRAM 带宽也很低 | + +> **关键洞察:** Memory Throughput 高(72%)但 DRAM Throughput 低(1.59%),说明数据主要在 **L1/L2 缓存层**被命中,而不是真正打满了 HBM 带宽。这是 `vectorAdd` 这类 streaming 访问的典型特征。 + +悬浮在 **Memory Throughput [%]** 指标上可以看到详细说明: +> 该指标对应 `gpu_compute_memory_throughput.avg.pct_of_peak_sustained_elapsed`,即在 elapsed cycles 期间,内存管线达到峰值持续速率的百分比。 + +### 2.2 Launch Statistics(启动配置统计) + +| 参数 | 值 | +|------|----| +| Grid Size | 8,192 | +| Block Size | 256 | +| Threads | 2,097,152(共 200 万线程) | +| Registers Per Thread | 16 | +| Waves Per SM | 9.48 | +| # SMs | 108 | +| Dynamic Shared Memory | 0 | + +- **Waves Per SM = 9.48** 意味着每个 SM 要连续调度约 9–10 波 warps,足以隐藏延迟。 +- 没有使用共享内存(0 bytes),符合 `vectorAdd` 的访存模式。 + +--- + +## 三、Details 页 — Occupancy(占用率) + +![alt text](4b9251f43cbde50fbf9b58b2251873e9.png) + + + +### 3.1 Occupancy 指标解读 + +| 指标 | 值 | +|------|----| +| Theoretical Occupancy [%] | **100** | +| Achieved Occupancy [%] | **71.63** | +| Theoretical Active Warps per SM | 64 | +| Achieved Active Warps Per SM | 45.84 | +| Block Limit Registers | 16 | +| Block Limit Shared Mem | 32 | +| Block Limit Warps | 8 | + +**理论占用率 100%**,说明 kernel 的寄存器和共享内存用量不会限制 SM 的 warp 容量。 + +**实际占用率只有 71.63%**,NCU 给出了解释: + +> 理论(100%)与实际(71.6%)之间的差距,可能来自 **warp 调度开销**或**负载不均衡**。 +> 负载不均衡可能发生在同一 block 内的 warps 之间,也可能发生在不同 block 之间。 + +NCU 估算修复该问题可带来约 **28.37% 的本地加速**。 + +### 3.2 GPU and Memory Workload Distribution + +| 指标 | 值 | +|------|-----| +| Average SM Active Cycles | 21,080.53 | +| Average L1 Active Cycles | 21,080.53 | +| Average L2 Active Cycles | 20,316.11 | +| Average DRAM Active Cycles | 21,918.70 | +| Total SM Elapsed Cycles | 2,289,698 | +| Total DRAM Elapsed Cycles | 1,215,488 | + +**SM Active Cycles ≈ L1 Active Cycles ≈ DRAM Active Cycles**,说明整个 kernel 执行期间 SM、L1、DRAM 的活跃时间高度一致——这是 streaming 型 kernel 的健康特征,不存在明显的等待气泡。 + +--- + +## 四、Details 页 — 底部警告与进阶建议 + + + +> **提示:** 图3 与图4 为同一截图,无需重复插图。可将图3 放置于 3.1 小节前,图4 可省略或替换为其他截图。 + +页面底部出现两条重要提示: + +1. **NVLink 警告:** + > 目标系统支持 NVLink,但本次 profile 未采集 NVLink sections。 + 如需分析多 GPU 通信,请在 profile 时加上 `--section NVLink`。 + +2. **缺少 Roofline 和 Memory Charts:** + > 请使用 `detailed` 或 `full` metric set 重新 profile,才能生成 Roofline 图和内存带宽图。 + + Roofline 模型是判断 kernel 是计算受限还是内存受限的黄金工具,强烈建议使用完整 metric set: + ```bash + ncu --set detailed -o my_report ./my_app + # 或 + ncu --set full -o my_report ./my_app + ``` + +--- + +## 五、总结:阅读 NCU 报告的标准流程 + +``` +Summary 页 + └─ 看 Duration、Memory Throughput、Compute Throughput + → 判断是内存瓶颈还是计算瓶颈 + ↓ +Details → GPU Speed of Light + └─ 确认瓶颈层级:DRAM / L2 / L1 / SM + ↓ +Details → Launch Statistics + └─ 检查 Grid/Block 配置、Waves Per SM 是否合理 + ↓ +Details → Occupancy + └─ 对比理论 vs 实际占用率,定位 warp 调度效率 + ↓ +Details → Workload Distribution + └─ 检查 SM、L1、L2、DRAM 活跃周期是否均衡 + ↓ +按需收集 detailed/full metric set → 查看 Roofline、Memory Charts +``` + +| 判断场景 | 典型表现 | 优化方向 | +|----------|----------|----------| +| 内存瓶颈(DRAM 高) | DRAM Throughput 接近峰值 | 合并访问、减少全局内存访问量 | +| 缓存瓶颈(L1 高) | L1/TEX Throughput 高,DRAM 低 | 优化数据局部性、使用共享内存 | +| 计算瓶颈(SM 高) | Compute Throughput 接近峰值 | 减少算术指令、使用 Tensor Core | +| 占用率不足 | Achieved < Theoretical | 减少寄存器/共享内存用量,调整 Block Size | diff --git "a/outputs/gpu-programming-course/code/chapter9/CUDA_NCU\346\212\245\345\221\212\351\230\205\350\257\273\346\225\231\347\250\213_WarpDivergence.md" "b/outputs/gpu-programming-course/code/chapter9/CUDA_NCU\346\212\245\345\221\212\351\230\205\350\257\273\346\225\231\347\250\213_WarpDivergence.md" new file mode 100644 index 0000000..b4a6b99 --- /dev/null +++ "b/outputs/gpu-programming-course/code/chapter9/CUDA_NCU\346\212\245\345\221\212\351\230\205\350\257\273\346\225\231\347\250\213_WarpDivergence.md" @@ -0,0 +1,78 @@ +# CUDA Nsight Compute(NCU)报告阅读--以 Warp Divergence 报告为例 + +这次用 `warp_divergence.ncu-rep` 这份报告做示例,里面对比了三个 kernel:`baselineKernel`(基准实现)、`divergentKernel`(含分支发散)、`coalescedKernel`。和上一份归约(reduce)教程一样,按"先看全局对比、再看单个 kernel 细节"的顺序来读。 + +## 一、报告整体结构 + +`.ncu-rep` 文件打开后固定有 `Summary`、`Details`、`Source` 等标签页。推荐顺序依然是:先在 Summary 里定位哪个 kernel 慢,再到 Details 里看 Speed Of Light(判断瓶颈类型)、Launch Statistics/Occupancy(判断并行度够不够)、Workload Distribution(判断负载是否均衡)。 + +## 二、Summary 页面:三个 kernel 的全局对比 + +![alt text](image-1.png) + +[NCU Summary 页面:baselineKernel / divergentKernel / coalescedKernel 对比](ncu2-summary.png) + +顶部信息行:当前选中的是第 890 号结果 `baselineKernel`,启动配置 `Size` 为 `(4096,1,1)x(256,1,1)`,单次耗时 `Time` 18.08 us,`Cycles` 22,938,运行设备是 `NVIDIA A800-SXM4-80GB`,`SM Frequency` 1.26 GHz,每线程占用 16 个寄存器。 + +中间表格把三个 kernel 横向列出来,关键列摘出来对比如下: + +| Kernel | Duration [us] | Compute Throughput [%] | Memory Throughput [%] | # Registers | +|---|---|---|---|---| +| baselineKernel | 18.08 | 67.57 | 11.41 | 16 | +| divergentKernel | ~101 | 72.11 | 3.77 | 16 | +| coalescedKernel | 53.44 | 70.07 | 7.22 | 16 | + +三者 Grid/Block 配置完全一致(都是 4096 个 block、256 线程/block),唯一的变量是 kernel 内部的代码逻辑,这也是这类对比实验的标准做法——固定启动配置,只改变计算/访问模式,这样性能差异才能完全归因到代码逻辑本身。一个反直觉的地方是:`divergentKernel` 的 Compute Throughput(72.11%)反而比 `baselineKernel`(67.57%)还高,但耗时却暴涨了 5 倍多——这正是 warp divergence 的典型信号,后面案例分析部分会展开讲。 + +表格下方的提示区里,`Achieved Occupancy` 给出的建议是:理论占用率 100%、实测占用率 82.9%,估算消除这部分差距能带来 **17.13%** 的本地加速(Est. Local Speedup),这个数字是针对当前选中的 `baselineKernel` 算出来的。 + +## 三、Details 页面(上半部分):GPU Speed Of Light Throughput 与 Launch Statistics + +下图是切换到 `baselineKernel` 的 Details 页面看到的内容。 + +![alt text](image-2.png) +[NCU Details 页面:Speed Of Light Throughput 与 Launch Statistics](ncu2-details-throughput.png) + +**GPU Speed Of Light Throughput**:Compute (SM) Throughput 67.57%、Memory Throughput 11.41%、L1/TEX Cache Throughput 7.16%、L2 Cache Throughput 20.64%、DRAM Throughput 11.41%。这组数字和前一份归约教程里的延迟瓶颈案例正好相反:Compute 远高于 Memory,NCU 自动弹出的提示也变成了 **High Compute Throughput**(而不是 Latency Issue),建议去看 `Compute Workload Analysis` 部分,确认计算单元具体在忙什么,以及"是否存在冗余计算、可以用查表(look-up table)代替"。这提示了一种常见的优化方向:如果计算量本身可以预先算好存表,就不需要每次都现场算。 + +**Launch Statistics**:Grid Size 4,096、Registers Per Thread 16、Block Size 256、Threads 1,048,576、Waves Per SM 4.74。Waves Per SM 表示要跑完所有 block 需要"轮"接近 5 批,说明这次的任务规模刚好能让 108 个 SM 都吃到饱(不存在某些 SM 闲置的尾部效应)。右边列出的共享内存相关字段都是 0,说明这几个 kernel 都没有主动申请共享内存,性能差异完全来自寄存器/分支/全局内存访问层面的代码逻辑。 + +## 四、Details 页面(下半部分):Occupancy 与 Workload Distribution + +继续往下滚动,是 Occupancy 和 Workload Distribution 两块面板。 + +![alt text](image-3.png) + +[NCU Details 页面:Occupancy 与 Workload Distribution](ncu2-details-occupancy.png) + +**Occupancy**: + +| 指标 | 数值 | +|---|---| +| Theoretical Occupancy [%] | 100 | +| Theoretical Active Warps per SM | 64 | +| Achieved Occupancy [%] | 82.87 | +| Achieved Active Warps Per SM | 53.04 | + +四个 Block Limit(Registers 16、Shared Mem 32、Warps 8、SM 32)里最小的是 Block Limit Warps=8,但理论占用率依然是 100%,说明 8 这个限制刚好等于硬件按当前 Block Size(256 线程=8 个 warp)能塞进去的数量,并不是真正的瓶颈。实测 82.87% 和理论值的差距,提示区解释为 warp 调度开销或 block/warp 间的负载不均衡,对应前面提到的 17.13% 本地加速空间。 + +**GPU and Memory Workload Distribution**:Average SM/L1 Active Cycles 20,100.20,对应的 Total SM/L1 Elapsed Cycles 是 2,497,620;Average L2 Active Cycles 17,727.44,Total L2 Elapsed Cycles 1,740,640;Average DRAM Active Cycles 只有 3,280.20,但 Total DRAM Elapsed Cycles 高达 1,149,952——DRAM 的"平均活跃周期/总经过周期"比值远低于 SM 和 L2,进一步印证了这是一个计算主导(compute-bound)而不是显存主导的 kernel,和前面 Speed Of Light 里 Memory Throughput 只有 11.41% 是一致的。 + +## 五、实战速查表 + +| 现象 | 可能原因 | 该看哪个面板 | +|---|---|---| +| Compute Throughput 远高于 Memory Throughput | 计算主导(compute-bound) | High Compute Throughput 提示 → Compute Workload Analysis | +| Compute Throughput 升高但 Duration 反而暴涨 | 大概率是 warp divergence,分支让 warp 内线程串行执行不同路径 | Source 页面看分支语句 + Warp State Statistics | +| Memory Throughput 随实现方式大幅波动,Grid/Block 配置不变 | 访问模式(是否合并访问)导致的差异 | Memory Workload Analysis / L1-L2-DRAM Throughput 拆解 | +| Theoretical 和 Achieved Occupancy 有差距但 Block Limit 都不小 | warp 调度开销或负载不均衡,不是资源配置问题 | Achieved Occupancy 提示 | + +## 六、案例解读:baseline / divergent / coalesced 三者对比说明了什么 + +把三个 kernel 的数据放在一起看,能讲清楚两个经典的 CUDA 性能话题。第一个是 **warp divergence(warp 内分支发散)**:`divergentKernel` 的 Compute Throughput(72.11%)比 `baselineKernel`(67.57%)还高,乍看像是"计算更努力",但 Duration 却从 18.08 us 涨到了约 101 us,Memory Throughput 也从 11.41% 跌到 3.77%。这是因为 GPU 以 warp(32 线程)为单位发射指令,如果同一个 warp 里的线程走进了不同的分支(比如 `if/else`),硬件会让整个 warp 把两条分支都执行一遍,未命中当前分支的线程被掩码(mask)成空闲但仍占着执行槛位——SM 看起来"很忙"(Compute Throughput 不低),但大量算力花在了被掩码线程的空转上,真正完成的有效计算反而更少,所以总耗时变长、内存访问的吞吐占比也被稀释。 + +第二个话题是**内存访问模式**:`coalescedKernel` 的耗时(53.44 us)介于另外两者之间,Memory Throughput(7.22%)比 baseline 还低一些。这提示一个常被忽视的点:kernel 名字带"coalesced"不代表它在这份对比里就是"最优"的那个——具体快慢还是要看 Source 页面里的实际访问模式和计算量,数据本身(Duration、Throughput 这些硬指标)永远比命名更可靠。这也是为什么读 NCU 报告时,要先看 Summary 表格里的真实数字,再去推断代码做了什么优化,而不是反过来。 + +## 七、小结 + +这份报告的特点和上一份归约 benchmark 正好相反:那边是延迟瓶颈(Compute、Memory 吞吐都很低),这边是计算主导(Compute Throughput 明显高于 Memory)。读 NCU 报告时,GPU Speed Of Light 这块面板给出的提示文字(Latency Issue / High Compute Throughput / Memory Bound 等)其实就是 NCU 自动帮你做的第一轮诊断,后面 Occupancy、Workload Distribution 这些面板则是在validate和细化这个诊断,最终要落到 Source 页面去看具体是哪一行代码造成的。 diff --git a/outputs/gpu-programming-course/code/chapter9/image-1.png b/outputs/gpu-programming-course/code/chapter9/image-1.png new file mode 100644 index 0000000..a0f1a50 Binary files /dev/null and b/outputs/gpu-programming-course/code/chapter9/image-1.png differ diff --git a/outputs/gpu-programming-course/code/chapter9/image-2.png b/outputs/gpu-programming-course/code/chapter9/image-2.png new file mode 100644 index 0000000..4726a17 Binary files /dev/null and b/outputs/gpu-programming-course/code/chapter9/image-2.png differ diff --git a/outputs/gpu-programming-course/code/chapter9/image-3.png b/outputs/gpu-programming-course/code/chapter9/image-3.png new file mode 100644 index 0000000..e2c7491 Binary files /dev/null and b/outputs/gpu-programming-course/code/chapter9/image-3.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-1.png b/outputs/gpu-programming-course/code/labs/image-1.png new file mode 100644 index 0000000..da61ed3 Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-1.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-10.png b/outputs/gpu-programming-course/code/labs/image-10.png new file mode 100644 index 0000000..36ddbb5 Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-10.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-11.png b/outputs/gpu-programming-course/code/labs/image-11.png new file mode 100644 index 0000000..50b17e7 Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-11.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-12.png b/outputs/gpu-programming-course/code/labs/image-12.png new file mode 100644 index 0000000..45fe5bd Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-12.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-13.png b/outputs/gpu-programming-course/code/labs/image-13.png new file mode 100644 index 0000000..84809c4 Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-13.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-14.png b/outputs/gpu-programming-course/code/labs/image-14.png new file mode 100644 index 0000000..e9e6d99 Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-14.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-15.png b/outputs/gpu-programming-course/code/labs/image-15.png new file mode 100644 index 0000000..31c0dab Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-15.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-16.png b/outputs/gpu-programming-course/code/labs/image-16.png new file mode 100644 index 0000000..58defe3 Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-16.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-17.png b/outputs/gpu-programming-course/code/labs/image-17.png new file mode 100644 index 0000000..0b95da3 Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-17.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-18.png b/outputs/gpu-programming-course/code/labs/image-18.png new file mode 100644 index 0000000..23b175a Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-18.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-19.png b/outputs/gpu-programming-course/code/labs/image-19.png new file mode 100644 index 0000000..68263cb Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-19.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-2.png b/outputs/gpu-programming-course/code/labs/image-2.png new file mode 100644 index 0000000..003ed7e Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-2.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-20.png b/outputs/gpu-programming-course/code/labs/image-20.png new file mode 100644 index 0000000..e188d0c Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-20.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-21.png b/outputs/gpu-programming-course/code/labs/image-21.png new file mode 100644 index 0000000..cd9b054 Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-21.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-22.png b/outputs/gpu-programming-course/code/labs/image-22.png new file mode 100644 index 0000000..6f5ee31 Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-22.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-23.png b/outputs/gpu-programming-course/code/labs/image-23.png new file mode 100644 index 0000000..0568d43 Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-23.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-24.png b/outputs/gpu-programming-course/code/labs/image-24.png new file mode 100644 index 0000000..fc026bf Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-24.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-3.png b/outputs/gpu-programming-course/code/labs/image-3.png new file mode 100644 index 0000000..80f53a8 Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-3.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-4.png b/outputs/gpu-programming-course/code/labs/image-4.png new file mode 100644 index 0000000..0fc8d76 Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-4.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-5.png b/outputs/gpu-programming-course/code/labs/image-5.png new file mode 100644 index 0000000..c3feb4e Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-5.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-6.png b/outputs/gpu-programming-course/code/labs/image-6.png new file mode 100644 index 0000000..2d1cfeb Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-6.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-7.png b/outputs/gpu-programming-course/code/labs/image-7.png new file mode 100644 index 0000000..46cc42b Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-7.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-8.png b/outputs/gpu-programming-course/code/labs/image-8.png new file mode 100644 index 0000000..85fcfac Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-8.png differ diff --git a/outputs/gpu-programming-course/code/labs/image-9.png b/outputs/gpu-programming-course/code/labs/image-9.png new file mode 100644 index 0000000..0fa47b4 Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image-9.png differ diff --git a/outputs/gpu-programming-course/code/labs/image.png b/outputs/gpu-programming-course/code/labs/image.png new file mode 100644 index 0000000..0fcace8 Binary files /dev/null and b/outputs/gpu-programming-course/code/labs/image.png differ diff --git a/outputs/gpu-programming-course/code/labs/lab1.md b/outputs/gpu-programming-course/code/labs/lab1.md new file mode 100644 index 0000000..cf8a444 --- /dev/null +++ b/outputs/gpu-programming-course/code/labs/lab1.md @@ -0,0 +1,135 @@ +# 如何读懂 lab1NCU(NVIDIA Nsight Compute)报告 + +本教程以 `sgemm_naive`(朴素矩阵乘法)内核为例,逐步解读 NCU 报告的三个核心视图:Summary、Details 上半部分(Speed of Light & Launch Statistics)、Details 下半部分(Occupancy & Workload Distribution)。 + +--- + +## 一、Summary(摘要)视图 + + +![图一](image-1.png) +[图1-NCU-Summary视图] + +Summary 视图是报告的入口,以表格形式展示所有内核调用的汇总数据。 + +**1.1 顶部元数据** + +| 字段 | 含义 | 本例数值 | +|------|------|---------| +| Result | 内核编号与名称 | 897 - sgemm_naive | +| Size | Grid × Block 配置 | (32, 32, 1) × (32, 32, 1) | +| Time | 单次执行时间 | 8.58 ms | +| Cycles | 经历的时钟周期数 | 10,940,126 | +| GPU | 使用的 GPU 型号 | NVIDIA A800-SXM4-80GB | +| SM Frequency | SM 实际主频 | 1.27 GHz | +| Registers/thread | 每线程寄存器数 | 32 | + +**1.2 表格各列解读** + +| 列名 | 说明 | 本例观察 | +|------|------|---------| +| Estimated Speedup [%] | NCU 估算的可优化空间 | 行 0–4 为 0.00(基准),行 5 起约 7.24–7.64% | +| Duration [ms] | 内核单次耗时 | 全部为 8.58 ms | +| Runtime Improvement [ms] | 相对基准可节省时间 | 约 0.62–0.66 ms | +| Compute Throughput [%] | SM 计算单元利用率 | **5.89%**(极低) | +| Memory Throughput [%] | 内存子系统利用率 | **93.92%**(极高) | + +> **关键结论**:Compute 利用率仅 5.89%,而 Memory 利用率高达 93.92%,这是典型的**内存瓶颈**内核——GPU 的算力几乎被闲置,全部时间都在等待内存数据。 + +--- + +## 二、Details 视图 —— GPU Speed of Light & Launch Statistics + + +![图二](image-2.png) +[图2-NCU-Details-SpeedOfLight](image2.png) + +Details 视图提供逐层细化的性能数据,分为多个可折叠区块。 + +**2.1 GPU Speed of Light Throughput(光速吞吐量)** + +该区块对比各硬件单元的实际利用率与理论峰值的比值: + +| 指标 | 数值 | 含义 | +|------|------|------| +| Compute (SM) Throughput [%] | 5.89 | SM 计算管线几乎空闲 | +| Memory Throughput [%] | 93.92 | 内存子系统接近饱和 | +| **L1/TEX Cache Throughput [%]** | **99.09** | **L1 缓存是真正的瓶颈所在** | +| L2 Cache Throughput [%] | 1.29 | L2 几乎不参与数据传输 | +| DRAM Throughput [%] | 0.07 | DRAM 几乎不参与数据传输 | + +Memory Throughput = 93.92% 是由 L1/TEX = 99.09% 拉高的,而非 DRAM(仅 0.07%)。这说明数据几乎全部命中 L1 缓存,**不存在 DRAM 带宽瓶颈**,但 L1 缓存的访问模式(例如 Bank Conflict 或未合并访问)很可能是问题所在。 + +NCU 给出了如下高亮提示(High Throughput): + +> 内核已使用超过 80% 的内存性能,要进一步提升性能,需要将工作从最繁忙的单元转移到其他单元。建议从 **Memory Workload Analysis → L1** 区块开始分析。 + +**2.2 Launch Statistics(启动统计)** + +| 指标 | 数值 | 说明 | +|------|------|------| +| Grid Size | 1,024 | 总 Block 数量 | +| Block Size | 1,024 | 每个 Block 的线程数(32×32) | +| Registers Per Thread | 32 | 每线程寄存器用量 | +| Threads | 1,048,576 | 总线程数(1024 blocks × 1024 threads) | +| Waves Per SM | 4.74 | 每个 SM 平均需轮转约 4.74 波次 | +| # SMs | 108 | GPU 共 108 个 SM | +| Static Shared Memory Per Block | 0 byte | **未使用任何共享内存** | +| Dynamic Shared Memory Per Block | 0 byte | **未使用任何共享内存** | + +> **重点**:`sgemm_naive` 完全依赖全局内存读取,没有使用任何共享内存(Shared Memory = 0 byte),这是其性能低下的根本原因之一,也指明了下一步优化方向——引入共享内存 Tiling。 + +--- + +## 三、Details 视图 —— Occupancy & 工作负载分布 + + +![图三](image-3.png) +[图3-NCU-Details-Occupancy](image3.png) + +**3.1 Occupancy(占用率)** + +占用率衡量 SM 上实际活跃 Warp 数与理论最大值的比例。高占用率有助于隐藏内存延迟: + +| 指标 | 数值 | 说明 | +|------|------|------| +| Theoretical Occupancy [%] | 100 | 理论上可达满占用 | +| Achieved Occupancy [%] | **93.80** | 实际达到的占用率,接近满载 | +| Theoretical Active Warps/SM | 64 | 理论每 SM 最多 64 个 Warp | +| Achieved Active Warps/SM | 60.03 | 实际平均每 SM 60 个 Warp 处于活跃 | +| Block Limit Registers [block] | 2 | 寄存器用量限制每 SM 最多 2 个 Block | +| Block Limit Shared Mem [block] | 8 | 共享内存限制(充足,不是瓶颈) | +| Block Limit Warps [block] | 2 | Warp 数量限制每 SM 最多 2 个 Block | +| Block Limit SM [block] | 32 | SM 硬件上限 | + +解读要点: +- 占用率已接近理论上限(93.8% vs 100%),说明**硬件调度效率已达极值**,不存在因 Warp 不足而导致的等待。 +- `Block Limit Registers = 2`:每个 Block 使用 32 寄存器/线程 × 1024 线程 = 32,768 个寄存器,恰好是限制每个 SM 只能同时跑 2 个 Block 的原因。 +- 瓶颈**不来自占用率**,而来自 L1/TEX 缓存的访问效率,需从访存模式入手优化。 + +**3.2 GPU and Memory Workload Distribution(工作负载分布)** + +| 指标 | 数值 | +|------|------| +| Average SM Active Cycles | 10,367,376 | +| Average L1 Active Cycles | 10,367,376 | +| Average L2 Active Cycles | 3,729,874 | +| Average DRAM Active Cycles | **9,836** | + +SM 活跃周期 ≈ L1 活跃周期,而 DRAM 活跃周期仅约 **9,836**(L1 的 0.09%),证明数据完全在 L1 缓存层面循环,DRAM 几乎不参与。这正是共享内存 Tiling 可以大幅改善的场景。 + +--- + +## 四、综合分析与优化方向 + +综合三个视图的数据: + +| 维度 | 诊断结论 | +|------|---------| +| 计算利用率 | 5.89%,严重不足,GPU 算力大量浪费 | +| 内存瓶颈层 | L1/TEX(99.09%),而非 DRAM(0.07%) | +| 占用率 | 93.8%,接近满载,**非瓶颈来源** | +| 共享内存 | 完全未使用(0 byte),是核心优化空间 | +| 访存模式 | 全局内存直接访问,存在 L1 重复读取 | + +**下一步优化建议**:引入**共享内存 Tiling(分块矩阵乘法)**,将全局内存的重复访问搬运到共享内存,降低 L1/TEX 压力,提升计算访存比(Arithmetic Intensity),预期可将执行时间缩短 30–40%。 diff --git a/outputs/gpu-programming-course/code/labs/lab2.md b/outputs/gpu-programming-course/code/labs/lab2.md new file mode 100644 index 0000000..b27c184 --- /dev/null +++ b/outputs/gpu-programming-course/code/labs/lab2.md @@ -0,0 +1,145 @@ +# Lab2 NCU 报告解读:全局内存合并访问(sgemm_global_mem_coalesce) + +本教程以 `sgemm_global_mem_coalesce`(全局内存合并访问矩阵乘法)内核为例,对照 Lab1 的 `sgemm_naive` 数据,逐步解读 NCU 报告的三个核心视图,帮助你理解合并访问优化的实际效果。 + +--- + +## 一、Summary(摘要)视图 + + +![图一](image.png) +[图1-NCU-Summary视图](image1.png) + +**1.1 顶部元数据** + +| 字段 | 含义 | 本例数值 | Lab1 对比 | +|------|------|---------|----------| +| Result | 内核编号与名称 | 891 - sgemm_global_mem_coalesce | sgemm_naive | +| Size | Grid × Block 配置 | (128, 128, 1) × (1024, 1, 1) | (32, 32, 1) × (32, 32, 1) | +| Time | 单次执行时间 | 46.32 ms | 8.58 ms(问题规模更小) | +| Cycles | 经历的时钟周期数 | 59,062,641 | 10,940,126 | +| GPU | GPU 型号 | NVIDIA A800-SXM4-80GB | 同 | +| SM Frequency | SM 实际主频 | 1.27 GHz | 1.27 GHz | + +> **注意**:本实验 Grid Size 为 (128, 128, 1),比 Lab1 的 (32, 32, 1) 大得多,对应更大的矩阵规模(约 4096×4096),因此绝对执行时间不能直接比较。 + +**1.2 表格各列解读** + +| 列名 | 本例数值 | Lab1 数值 | 变化含义 | +|------|---------|----------|---------| +| Estimated Speedup [%] | 0.00% | 0.00%(基准行) | NCU 未发现可量化的优化空间 | +| Duration [ms] | 46.32 | 8.58 | 问题规模扩大,不可直接比较 | +| Runtime Improvement [ms] | 0.00 | 0.62–0.66 | 当前内核已无可量化提升 | +| **Compute Throughput [%]** | **67.32** | **5.89** | **↑ 10× 以上,计算单元大幅活跃** | +| **Memory Throughput [%]** | **67.33** | **93.92** | **↓ 显著降低,内存不再是单一瓶颈** | + +> **关键结论**:Compute(67.32%)与 Memory(67.33%)几乎完全相等,说明优化后的内核进入了**计算与访存均衡**的状态,这是 NCU 理想的优化目标。 + +--- + +## 二、Details 视图 —— GPU Speed of Light & Launch Statistics + + +![图二](image-4.png) +[图2-NCU-Details-SpeedOfLight](image2.png) + +**2.1 GPU Speed of Light Throughput(光速吞吐量)** + +| 指标 | 本例数值 | Lab1 数值 | 变化含义 | +|------|---------|----------|---------| +| Compute (SM) Throughput [%] | 67.32 | 5.89 | **↑ 11× ,计算单元被充分利用** | +| Memory Throughput [%] | 67.33 | 93.92 | ↓ 内存不再饱和 | +| **L1/TEX Cache Throughput [%]** | **67.61** | **99.09** | **↓ 31%,L1 不再是瓶颈** | +| L2 Cache Throughput [%] | 10.64 | 1.29 | ↑ L2 开始参与数据传输 | +| DRAM Throughput [%] | 9.32 | 0.07 | **↑ 130× ,数据真正流向 DRAM** | + +**核心变化解读**: + +- Lab1 中,L1/TEX = 99.09% 但 DRAM 仅 0.07%,说明数据在 L1 层"原地打转",大量冗余重复读取。 +- Lab2 中,L1/TEX 降至 67.61%,DRAM 升至 9.32%,L2 升至 10.64%——数据开始沿正确路径从 DRAM → L2 → L1 流动,冗余读取大幅减少。 +- Compute 与 Memory 吞吐率均约 67%,说明 **GPU 的算力和带宽被同步利用**,这是全局内存合并访问带来的直接收益。 + +NCU 给出了**Balanced Throughput(均衡吞吐)**提示: + +> Compute 与 Memory 已处于均衡状态。要进一步降低执行时间,需要同时减少计算量和内存流量。建议查看 **Compute Workload Analysis** 和 **Memory Workload Analysis** 两个区块。 + +这与 Lab1 的"High Throughput(内存占主导)"提示形成鲜明对比——内核的性质已从单纯内存瓶颈升级为计算访存共同瓶颈。 + +**2.2 Launch Statistics(启动统计)** + +| 指标 | 本例数值 | Lab1 数值 | 说明 | +|------|---------|----------|------| +| Grid Size | 16,384 | 1,024 | Block 总数扩大 16× | +| Block Size | 1,024 | 1,024 | 每 Block 线程数相同(32×32=1024) | +| Registers Per Thread | 32 | 32 | 寄存器用量相同 | +| Threads | 16,777,216 | 1,048,576 | 总线程数扩大 16× | +| **Waves Per SM** | **75.85** | **4.74** | **每 SM 需轮转波次大幅增加** | +| # SMs | 108 | 108 | 同一 GPU | +| Static/Dynamic Shared Memory | 0 byte | 0 byte | 仍未使用共享内存 | + +> **重点**:Waves Per SM 从 4.74 跃升至 75.85,意味着每个 SM 要依次处理多得多的 Block 波次,整体工作量远大于 Lab1。Shared Memory 依然为 0,表明 **合并访问解决了访问效率问题,但未引入数据复用机制**,这是 Lab3(共享内存 Tiling)的优化空间。 + +--- + +## 三、Details 视图 —— Occupancy & 工作负载分布 + + +![图三](image-5.png) +[图3-NCU-Details-Occupancy](image3.png) + +**3.1 Occupancy(占用率)** + +| 指标 | 本例数值 | Lab1 数值 | 变化 | +|------|---------|----------|------| +| Theoretical Occupancy [%] | 100 | 100 | 不变 | +| **Achieved Occupancy [%]** | **99.78** | **93.80** | **↑ 5.98%,几乎达到理论上限** | +| Theoretical Active Warps/SM | 64 | 64 | 不变 | +| Achieved Active Warps/SM | 63.86 | 60.03 | ↑ 每 SM 多跑约 3.8 个 Warp | +| Block Limit Registers | 2 | 2 | 相同(32 寄存器/线程 × 1024 线程限制) | +| Block Limit Shared Mem | 8 | 8 | 相同 | +| Block Limit Warps | 2 | 2 | 相同 | +| Block Limit SM | 32 | 32 | 相同 | + +解读要点: +- 占用率从 93.80% 提升至 99.78%,已极度接近理论满占用,说明 **每个 SM 几乎所有时间都有 Warp 可以调度**,SM 资源利用率已达极值。 +- Block Limit 约束与 Lab1 完全相同(寄存器仍是主要限制因素),结构配置未改变。 +- 更高的占用率有助于掩盖内存延迟,这也是 Compute Throughput 提升的原因之一。 + +**3.2 GPU and Memory Workload Distribution(工作负载分布)** + +| 指标 | 本例数值 | Lab1 数值 | 变化含义 | +|------|---------|----------|---------| +| Average SM Active Cycles | 58,851,784 | 10,367,376 | 总计算量扩大(更大矩阵) | +| Average L1 Active Cycles | 58,851,784 | 10,367,376 | L1 与 SM 同步活跃 | +| **Average L2 Active Cycles** | **56,230,860** | **3,729,874** | **↑ 15× ,L2 大量参与数据传输** | +| **Average DRAM Active Cycles** | **6,877,940** | **9,836** | **↑ 700× ,DRAM 被真正利用** | + +这组数据最直观地揭示了合并访问优化的本质: + +- Lab1 的 DRAM 活跃周期仅 9,836,说明几乎所有数据"卡在" L1 层反复读取,从未真正到达 DRAM。 +- Lab2 的 DRAM 活跃周期达到 6,877,940(增长约 700 倍),L2 也活跃了 56M 个周期——**全局内存访问真正沿内存层级逐级流动**,这正是合并访问的核心作用:将原来的随机/重复 L1 读取,变成按 Warp 对齐的高效连续 DRAM 访问。 + +--- + +## 四、综合分析与优化方向 + +**与 Lab1(sgemm_naive)的核心对比** + +| 维度 | Lab1(Naive) | Lab2(合并访问) | 变化 | +|------|-------------|----------------|------| +| 计算利用率 | 5.89% | 67.32% | **↑ 11×** | +| 内存利用率 | 93.92% | 67.33% | ↓ 均衡化 | +| L1/TEX 吞吐 | 99.09% | 67.61% | ↓ 不再是瓶颈 | +| DRAM 吞吐 | 0.07% | 9.32% | **↑ 130×** | +| NCU 提示类型 | High Throughput(内存主导) | **Balanced Throughput(均衡)** | 质变 | +| Achieved Occupancy | 93.80% | 99.78% | ↑ 接近满占用 | +| 共享内存使用 | 0 byte | 0 byte | 均未使用 | + +**本阶段瓶颈分析** + +合并访问解决了 Lab1 中 L1 层的随机访问问题,使内核进入计算访存均衡状态。但仍有两个主要瓶颈: + +1. **每次 K 维循环仍有重复全局内存读取**:A 矩阵的同一行被多个线程反复从全局内存加载,B 矩阵的同一列同理。 +2. **共享内存完全未使用**:没有数据复用机制,无法避免重复读取。 + +**下一步优化建议**:引入**共享内存 Tiling(分块矩阵乘法)**,通过将矩阵数据块预加载到共享内存,使每块数据只需从 DRAM 读取一次,再被 Block 内所有线程复用,从而显著降低 DRAM 带宽压力并提升 Arithmetic Intensity。 diff --git a/outputs/gpu-programming-course/code/labs/lab3.md b/outputs/gpu-programming-course/code/labs/lab3.md new file mode 100644 index 0000000..4b01d7c --- /dev/null +++ b/outputs/gpu-programming-course/code/labs/lab3.md @@ -0,0 +1,166 @@ +# Lab3 NCU 报告解读:共享内存分块(sgemm_shared_mem_block) + +本教程以 `sgemm_shared_mem_block`(共享内存 Tiling 矩阵乘法)内核为例,对照 Lab2(合并访问)的数据,逐步解读 NCU 报告的三个核心视图,帮助你理解共享内存分块优化的实际效果与新出现的瓶颈。 + +--- + +## 一、Summary(摘要)视图 + + +![图一](image-6.png) +[图1-NCU-Summary视图](image1.png) + +**1.1 顶部元数据** + +| 字段 | 含义 | 本例数值 | Lab2 对比 | +|------|------|---------|----------| +| Result | 内核编号与名称 | 897 - sgemm_shared_mem_block | sgemm_global_mem_coalesce | +| Size | Grid × Block 配置 | (128, 128, 1) × (1024, 1, 1) | (128, 128, 1) × (1024, 1, 1) | +| Time | 单次执行时间 | **28.40 ms** | 46.32 ms | +| Cycles | 经历的时钟周期数 | 36,213,173 | 59,062,641 | +| GPU | GPU 型号 | NVIDIA A800-SXM4-80GB | 同 | +| SM Frequency | SM 实际主频 | 1.27 GHz | 1.27 GHz | + +**1.2 表格各列解读** + +表格共 2 行(ID=0 和 ID=1),均对应同一内核的两次运行: + +| 列名 | 本例数值 | Lab2 数值 | 变化含义 | +|------|---------|----------|---------| +| Estimated Speedup [%] | 0.00% | 0.00% | NCU 未发现可量化的优化空间 | +| Duration [ms] | 28.40 | 46.32 | **↓ 38.7%,执行时间显著缩短** | +| Runtime Improvement [ms] | 0.00 | 0.00 | 无进一步可量化提升 | +| **Compute Throughput [%]** | **75.57** | **67.32** | **↑ 12.3%,计算单元更充分** | +| **Memory Throughput [%]** | **89.76** | **67.33** | **↑ 33.3%,内存子系统利用率回升** | + +> **关键结论**:执行时间从 46.32 ms 降至 28.40 ms,缩短了约 **38.7%**。但与此同时,Memory Throughput 重新拉高到 89.76%,NCU 的提示也从 Lab2 的「Balanced Throughput」重新变回「**High Throughput**」,说明共享内存的引入在提升计算效率的同时,**带来了新的内存访问瓶颈**。 + +--- + +## 二、Details 视图 —— GPU Speed of Light & Launch Statistics + + +![alt text](image-7.png) +[图2-NCU-Details-SpeedOfLight](image2.png) + +**2.1 GPU Speed of Light Throughput(光速吞吐量)** + +| 指标 | 本例数值 | Lab2 数值 | Lab1 数值 | 变化含义 | +|------|---------|----------|----------|---------| +| Compute (SM) Throughput [%] | 75.57 | 67.32 | 5.89 | ↑ 持续提升,计算占比更高 | +| Memory Throughput [%] | 89.76 | 67.33 | 93.92 | ↑ 重新回升,内存再次成为主要瓶颈 | +| **L1/TEX Cache Throughput [%]** | **89.99** | **67.61** | **99.09** | **↑ 33%,L1 再次接近饱和** | +| L2 Cache Throughput [%] | 14.88 | 10.64 | 1.29 | ↑ L2 参与度略有提升 | +| DRAM Throughput [%] | 15.17 | 9.32 | 0.07 | ↑ DRAM 持续被合理利用 | + +**核心变化解读**: + +Lab3 与 Lab1 的 L1/TEX 都接近 90–99%,但**瓶颈本质完全不同**: + +- **Lab1**:L1 高是因为全局内存随机访问导致大量 Cache Miss,数据在 L1 层反复等待,DRAM 几乎不动(0.07%)。 +- **Lab3**:L1 高是因为每个线程通过共享内存进行大量数据复用,DRAM 已被合理利用(15.17%),L2 也更活跃(14.88%)。这里 L1 的瓶颈来自**共享内存的 Bank Conflict**,而不是全局内存的低效访问。 + +**重要对比**: + +| 瓶颈来源 | Lab1(Naive) | Lab3(共享内存) | +|---------|-------------|----------------| +| L1/TEX 高的原因 | 全局内存随机访问,Cache Miss | 共享内存 Bank Conflict | +| DRAM 利用率 | 0.07%(几乎不工作) | 15.17%(正常工作) | +| 可优化方向 | 改为合并访问 | 消除 Bank Conflict | + +NCU 给出了**High Throughput(高吞吐)**提示: + +> 内核已使用超过 80% 的内存性能,要进一步提升,需要将工作从最繁忙的单元转移到其他单元。建议从 **Memory Workload Analysis → L1** 区块开始分析。 + +**2.2 Launch Statistics(启动统计)** + +| 指标 | 本例数值 | Lab2 数值 | 变化说明 | +|------|---------|----------|---------| +| Grid Size | 16,384 | 16,384 | 相同 | +| Block Size | 1,024 | 1,024 | 相同 | +| Registers Per Thread | 32 | 32 | 相同 | +| Threads | 16,777,216 | 16,777,216 | 相同 | +| Waves Per SM | 75.85 | 75.85 | 相同 | +| **Static Shared Memory Per Block** | **8.19 Kbyte** | **0 byte** | **↑ 首次使用共享内存!** | +| Dynamic Shared Memory Per Block | 0 | 0 | 无动态分配 | +| Shared Memory Configuration Size | 32.77 Kbyte | 8.19 Kbyte | SM 共享内存配置扩大 | +| # SMs | 108 | 108 | 相同 | + +> **核心变化**:Static Shared Memory Per Block 从 0 变为 **8.19 Kbyte/block**,这是 Lab3 相比 Lab2 最本质的结构性改变。每个 Block 分配了约 8 KB 的共享内存,用于缓存矩阵 A 和 B 的分块数据,从而将对全局内存的重复读取转变为对共享内存的复用读取。 + +--- + +## 三、Details 视图 —— Occupancy & 工作负载分布 + + +![alt text](image-8.png) +[图3-NCU-Details-Occupancy](image3.png) + +**3.1 Occupancy(占用率)** + +| 指标 | 本例数值 | Lab2 数值 | 变化 | +|------|---------|----------|------| +| Theoretical Occupancy [%] | 100 | 100 | 不变 | +| Achieved Occupancy [%] | **99.62** | **99.78** | 基本持平,略微下降 | +| Theoretical Active Warps/SM | 64 | 64 | 不变 | +| Achieved Active Warps/SM | 63.76 | 63.86 | 基本持平 | +| Block Limit Registers [block] | 2 | 2 | 相同(寄存器仍是主限制) | +| **Block Limit Shared Mem [block]** | **3** | **8** | **↓ 共享内存开始参与限制** | +| Block Limit Warps [block] | 2 | 2 | 相同 | +| Block Limit SM [block] | 32 | 32 | 相同 | + +解读要点: + +- **Block Limit Shared Mem 从 8 降至 3**:每个 SM 有约 32 KB 可用共享内存,每个 Block 使用了 8.19 KB,因此 32 / 8.19 ≈ 3.9,向下取整为 **3**。这意味着共享内存已经成为限制每个 SM 能运行多少 Block 的约束之一(虽然寄存器 Block Limit=2 仍更严格,实际每 SM 同时运行 2 个 Block)。 +- **Achieved Occupancy 99.62% ≈ Lab2 的 99.78%**:占用率几乎没有变化,说明共享内存的引入没有显著降低 SM 的调度效率。 +- 这意味着 Lab3 的性能提升(时间缩短 38.7%)**完全来自共享内存 Tiling 减少了全局内存访问次数**,而非占用率的变化。 + +**3.2 GPU and Memory Workload Distribution(工作负载分布)** + +| 指标 | 本例数值 | Lab2 数值 | 变化含义 | +|------|---------|----------|---------| +| Average SM Active Cycles | 36,109,085 | 58,851,784 | ↓ 总执行周期大幅减少 | +| Average L1 Active Cycles | 36,109,085 | 58,851,784 | ↓ L1 活跃周期同步减少 | +| **Average L2 Active Cycles** | **34,478,692** | **56,230,860** | **↓ 显著减少,L2 压力降低** | +| **Average DRAM Active Cycles** | **6,865,535** | **6,877,940** | 几乎持平 | + +关键发现: + +- SM / L1 活跃周期从 58.8M 降至 36.1M(↓39%),与执行时间缩短比例吻合,说明共享内存 Tiling 整体减少了计算和访存的总工作量。 +- **DRAM 活跃周期几乎不变**(6,865,535 vs 6,877,940):这说明每次从 DRAM 取到的数据量本身没有显著变化,但**每块数据被复用的次数大幅增加**——同样的数据,Lab3 让更多线程从共享内存中读取,而不是每个线程都去访问全局内存。 +- L2 活跃周期从 56.2M 降至 34.5M(↓39%),进一步验证全局内存流量大幅减少。 + +--- + +## 四、综合分析与优化方向 + +**三个实验的横向对比** + +| 维度 | Lab1(Naive) | Lab2(合并访问) | Lab3(共享内存) | +|------|-------------|----------------|----------------| +| 执行时间 | 8.58 ms(小矩阵) | 46.32 ms | **28.40 ms** | +| Compute Throughput | 5.89% | 67.32% | **75.57%** | +| Memory Throughput | 93.92% | 67.33% | **89.76%** | +| L1/TEX Throughput | 99.09% | 67.61% | **89.99%** | +| L2 Throughput | 1.29% | 10.64% | **14.88%** | +| DRAM Throughput | 0.07% | 9.32% | **15.17%** | +| NCU 提示类型 | High(L1 饱和) | **Balanced(均衡)** | High(L1 再次饱和) | +| Achieved Occupancy | 93.80% | 99.78% | **99.62%** | +| 共享内存/Block | 0 byte | 0 byte | **8.19 Kbyte** | +| DRAM 活跃周期 | 9,836 | 6,877,940 | 6,865,535 | + +**当前瓶颈:共享内存 Bank Conflict** + +DRAM 吞吐仅 15.17%,而 L1/TEX 高达 89.99%,证明主要瓶颈已从全局内存转移到了**共享内存内部访问冲突**。 + +具体来说,Lab3 的内积计算通常写成: + +```cpp +for (int dotIdx = 0; dotIdx < BLOCKSIZE; ++dotIdx) { + tmp += As[threadRow][dotIdx] * Bs[dotIdx][threadCol]; +} +``` + +当多个线程在同一 Warp 内同时访问 `As[threadRow][dotIdx]`(不同 `threadRow`,相同 `dotIdx`)时,它们会映射到共享内存的**同一 Bank**,产生 Bank Conflict,导致访问被串行化,拉高 L1/TEX 利用率但降低有效吞吐。 + +**下一步优化建议**:通过**矩阵转置**(将 `As` 以列优先布局存储)或**调整访问步长**来错开 Bank 访问,消除 Bank Conflict,将 L1/TEX 的高占用率真正转化为有效的数据吞吐。 diff --git a/outputs/gpu-programming-course/code/labs/lab4.md b/outputs/gpu-programming-course/code/labs/lab4.md new file mode 100644 index 0000000..27adfe1 --- /dev/null +++ b/outputs/gpu-programming-course/code/labs/lab4.md @@ -0,0 +1,160 @@ +# Lab4:SGEMM 1D Block Tiling 性能分析 + +## 实验目标 + +在 Lab3 共享内存分块(Shared Memory Tiling)的基础上,进一步引入 **1D Block Tiling**——让每个线程负责计算输出矩阵 C 中同一列方向上的 TN 个元素,从而提升每次共享内存加载的计算复用率,降低访存压力。 + +--- +![alt text](image-9.png) +1. Kernel 启动配置 + +本次实验 Kernel 为 `sgemm1DBlocktiling`,矩阵规模 4096×4096,使用以下启动参数: + +| 参数 | 数值 | +|------|------| +| Grid Size | (64, 64, 1) = 4096 个 Block | +| Block Size | 512 threads | +| 总线程数 | 2,097,152 | +| 每 Block 静态共享内存 | 4.10 KB | +| 每 Block 动态共享内存 | 0 | +| 每线程寄存器数 | 46 | +| Waves Per SM | 18.96 | +| # SMs | 108 | + +每个 Block 覆盖 C 矩阵的 64×64 子块,Block 内 512 个线程各自负责该子块中 **8 列**(TN=8)的结果,即 1D 方向上的 Thread-level Tiling。 + +--- +![alt text](image-10.png) +2. 核心性能指标 + +| 指标 | 数值 | +|------|------| +| 执行时间(单次)| 14.56 ms | +| SM 频率 | 1.27 GHz | +| 总 Elapsed Cycles | 18,569,403 | +| SM Active Cycles | 18,505,161.90 | +| DRAM 频率 | 1.59 GHz | + +--- + +3. 吞吐量分析 + +| 指标 | 数值 | +|------|------| +| Compute (SM) Throughput | 53.54% | +| Memory Throughput | 79.46% | +| L1/TEX Cache Throughput | 79.79% | +| L2 Cache Throughput | 13.95% | +| DRAM Throughput | 9.09% | + +**关键观察**:Memory Throughput(79.46%)显著高于 Compute Throughput(53.54%),Nsight 给出 **"High Memory Throughput"** 警告,说明当前 Kernel 仍处于**访存瓶颈**状态。 + +进一步拆解访存层级: +- L1/TEX Throughput 高达 **79.79%**,而 DRAM Throughput 仅 **9.09%** +- 这表明大部分访存流量来自 **L1/共享内存层**,全局内存(DRAM)已被有效遮盖 +- 瓶颈已从 Lab3 的"全局内存未合并访问"转移到**共享内存内部访问效率**上 + +--- +![alt text](image-11.png) +4. Occupancy 分析 + +| 指标 | 数值 | +|------|------| +| Theoretical Occupancy | 50% | +| Achieved Occupancy | 49.38% | +| Theoretical Active Warps per SM | 32 | +| Achieved Active Warps per SM | 31.61 | +| Block Limit Registers(绑定约束)| **2 blocks/SM** | +| Block Limit Shared Mem | 6 blocks/SM | +| Block Limit Warps | 4 blocks/SM | +| Block Limit SM | 32 blocks/SM | + +理论占用率仅 **50%**,根本原因是**寄存器用量过高**: + +- 每线程使用 46 个寄存器 +- A800-SXM4-80GB 每 SM 共有 65536 个寄存器,Block Size=512 threads +- 每 Block 需要 512 × 46 = 23,552 个寄存器 +- 65536 ÷ 23,552 ≈ **2.78**,向下取整得 **Block Limit Registers = 2** +- 即每 SM 最多同时运行 2 个 Block = 32 个 Warp,占理论最大 64 Warp 的 50% + +Nsight 给出提示: + +> 8.00 theoretical warps per scheduler this kernel can issue according to its occupancy are below the hardware maximum of 16. This kernel's theoretical occupancy (50.0%) is limited by the number of required registers. + +Achieved Occupancy 为 49.38%,与理论值 50% 几乎吻合,说明负载分布均匀,不存在明显的 Warp 调度浪费。 + +--- + +5. GPU 与内存工作负载分布 + +| 指标 | 数值 | +|------|------| +| Average SM Active Cycles | 18,505,161.90 | +| Average L1 Active Cycles | 18,505,161.90 | +| Average L2 Active Cycles | 17,614,197.09 | +| Average DRAM Active Cycles | 2,109,977.30 | +| Total SM Elapsed Cycles | 2,006,702,282 | +| Total L2 Elapsed Cycles | 1,415,477,840 | +| Total DRAM Elapsed Cycles | 928,069,120 | +| Total SMSP Elapsed Cycles | 8,026,809,128 | + +SM Active Cycles 与 L1 Active Cycles 完全对齐,L2 活跃周期也极高,进一步印证计算单元在几乎全程等待 **L1/共享内存**返回数据。 + +--- + +6. 瓶颈定位:共享内存 Bank Conflict + +当前 DRAM 吞吐仅 9.09%,说明全局内存带宽已不是主要矛盾;L1/TEX 高达 79.79%,说明**共享内存访问**是当前瓶颈。 + +在 1D Block Tiling 的内层循环中,每个线程读取共享内存 `As` 和 `Bs` 的访问模式如下: + +```cpp +for (int dotIdx = 0; dotIdx < BLOCKSIZE; ++dotIdx) { + // 每个线程读 As 的同一行,Bs 的 TN 个不同列 + float a = As[innerRowA * BLOCKSIZE + dotIdx]; + for (int resIdx = 0; resIdx < TN; ++resIdx) { + tmp[resIdx] += a * Bs[dotIdx * BN + innerColB * TN + resIdx]; + } +} +``` + +多个线程同时访问 `Bs` 的相邻列,若 `BN`(block N 方向大小)是 32 的倍数,则不同线程可能落在同一 Bank,引发 **Bank Conflict**,导致共享内存访问被串行化。 + +--- + +7. 与 Lab3 对比 + +| 指标 | Lab3(共享内存 Tiling)| Lab4(1D Block Tiling)| 变化 | +|------|----------------------|----------------------|------| +| Compute (SM) Throughput | ~67.32% | 53.54% | ↓ 13.8% | +| Memory Throughput | ~89.76% | 79.46% | ↓ 10.3% | +| L1/TEX Throughput | ~89.99% | 79.79% | ↓ 10.2% | +| DRAM Throughput | ~15.17% | 9.09% | ↓ 6.1% | +| Theoretical Occupancy | 100% | 50% | ↓ 50% | +| Achieved Occupancy | ~99.62% | 49.38% | ↓ 50.2% | +| 执行时间(单次)| ~28.40 ms | 14.56 ms | **↓ 48.7%** | + +核心结论:1D Block Tiling 通过提升每次内存加载的**计算复用率**,使执行时间缩短了近 **49%**,尽管 Occupancy 因寄存器压力减半,但更密集的计算量弥补了并行度的损失。DRAM 吞吐大幅下降,说明全局内存访问已被有效缓解,瓶颈收窄至**共享内存层**。 + +--- + +8. 下一步优化方向 + +8.1 消除共享内存 Bank Conflict → 2D Block Tiling + +引入 **2D Thread-level Tiling**(每个线程同时计算 TM×TN 个输出元素),并对共享内存的存储布局进行转置或 Padding,使不同线程对 `Bs` 的访问分散到不同 Bank: + +```cpp +// 对 Bs 转置存储,列访问变行访问,消除 Bank Conflict +Bs[dotIdx + resIdx * BLOCKSIZE] = B[...]; +``` + +8.2 寄存器压力优化 → 减少 TN 或重构循环 + +当前每线程 46 个寄存器导致占用率仅 50%。可以尝试: +- 减小 TN(如从 8 降至 4),降低每线程寄存器需求 +- 使用 `__launch_bounds__` 向编译器提示目标占用率,引导寄存器分配策略 + +8.3 向量化访存 → float4 加载 + +对全局内存到共享内存的搬运阶段,使用 `float4` 向量化加载,一条指令搬运 16 字节,提升内存带宽利用率并减少指令数。 diff --git a/outputs/gpu-programming-course/code/labs/lab5.md b/outputs/gpu-programming-course/code/labs/lab5.md new file mode 100644 index 0000000..11d7fd9 --- /dev/null +++ b/outputs/gpu-programming-course/code/labs/lab5.md @@ -0,0 +1,171 @@ +# Lab5:SGEMM 2D Block Tiling 性能分析 + +## 实验目标 + +在 Lab4(1D Block Tiling)的基础上,引入 **2D Thread-level Tiling**——每个线程同时计算输出矩阵 C 中 TM×TN 个元素(行列两个方向同时展开),进一步提升计算复用率,并通过调整共享内存访问模式消除 Bank Conflict。 + +--- + +1. Kernel 启动配置 + +本次实验 Kernel 为 `sgemm2DBlocktiling`,使用以下启动参数: + +| 参数 | 数值 | +|------|------| +| Grid Size | (32, 32, 1) = 1,024 个 Block | +| Block Size | 256 threads | +| 总线程数 | 262,144 | +| 每 Block 静态共享内存 | 8.19 KB | +| 每 Block 动态共享内存 | 0 | +| 每线程寄存器数 | **123** | +| Waves Per SM | 4.74 | +| # SMs | 108 | + +相比 Lab4(512 threads/block,46 寄存器/线程),2D Tiling 每线程需承载 TM×TN 个累加寄存器,寄存器用量从 46 暴增至 **123**,Block Size 也从 512 缩减至 256,是 Occupancy 大幅下降的根本原因。 + + +![alt text](image-12.png) +[图1:Nsight Compute Summary —— 多次运行概览与优化建议](image1.png) + +--- + +2. 核心性能指标 + +| 指标 | 数值 | +|------|------| +| 执行时间(单次)| 10.61 ms | +| SM 频率 | 1.27 GHz | +| 总 Elapsed Cycles | 13,521,254 | +| SM Active Cycles | 12,845,055.53 | +| DRAM 频率 | 1.59 GHz | + +--- + +3. 吞吐量分析 + +| 指标 | 数值 | +|------|------| +| Compute (SM) Throughput | **74.99%** | +| Memory Throughput | 59.39% | +| L1/TEX Cache Throughput | 62.64% | +| L2 Cache Throughput | 9.67% | +| DRAM Throughput | 3.84% | + +**关键转变**:Compute Throughput(74.99%)首次**超越** Memory Throughput(59.39%),Nsight 警告由 Lab4 的 "High Memory Throughput" 变为 **"High Compute Throughput"**。 + +这意味着 2D Block Tiling 成功将 Kernel 从**访存瓶颈**转变为**计算瓶颈**: + +- 每次从共享内存加载一个元素后,被 TM×TN 个线程共同复用,算术强度大幅提升 +- DRAM Throughput 已降至仅 **3.84%**(Lab4 为 9.09%),全局内存访问几乎完全被缓存层遮盖 +- L1/TEX Throughput(62.64%)相比 Lab4(79.79%)显著下降,说明共享内存 Bank Conflict 已得到有效缓解 + + +![alt text](image-13.png) +[图2:Nsight Compute Details —— GPU Speed of Light 与 Launch Statistics](image2.png) + +--- + +4. Occupancy 分析 + +| 指标 | 数值 | +|------|------| +| Theoretical Occupancy | **25%** | +| Achieved Occupancy | 23.76% | +| Theoretical Active Warps per SM | 16 | +| Achieved Active Warps per SM | 15.21 | +| Block Limit Registers(绑定约束)| **2 blocks/SM** | +| Block Limit Shared Mem | 7 blocks/SM | +| Block Limit Warps | 8 blocks/SM | +| Block Limit SM | 32 blocks/SM | + +理论占用率仅 **25%**,绑定约束仍为**寄存器数量**: + +- 每线程使用 123 个寄存器,Block Size=256 threads +- 每 Block 需要 256 × 123 = 31,488 个寄存器 +- 65536 ÷ 31,488 ≈ **2.08**,向下取整得 **Block Limit Registers = 2** +- 即每 SM 仅能容纳 2 个 Block = 16 个 Warp,占理论最大 64 Warp 的 **25%** + +Nsight 警告: + +> The 4.00 theoretical warps per scheduler this kernel can issue according to its occupancy are below the hardware maximum of 16. This kernel's theoretical occupancy (25.0%) is limited by the number of required registers. + +尽管 Occupancy 从 Lab4 的 50% 进一步减半至 25%,执行时间仍从 14.56 ms 缩短到 10.61 ms。原因在于每个线程完成了更多的有效计算,**计算密度**的提升抵消了并行度下降的代价。 + +Achieved Occupancy(23.76%)与理论值(25%)高度吻合,说明负载分布均匀。 + + +![alt text](image-14.png) +[图3:Nsight Compute Details —— Occupancy 分析与 GPU/内存工作负载分布](image3.png) + +--- + +5. GPU 与内存工作负载分布 + +| 指标 | 数值 | +|------|------| +| Average SM Active Cycles | 12,845,055.53 | +| Average L1 Active Cycles | 12,845,055.53 | +| Average L2 Active Cycles | 8,110,975.71 | +| Average DRAM Active Cycles | 649,232.30 | +| Total SM Elapsed Cycles | 1,463,253,382 | +| Total L2 Elapsed Cycles | 1,030,448,480 | +| Total SMSP Elapsed Cycles | 5,853,013,528 | +| Total DRAM Elapsed Cycles | 675,799,040 | + +DRAM Active Cycles(649,232)相比 Lab4(2,109,977)下降了约 **69%**,全局内存访问已被极大压缩。SM 与 L1 Active Cycles 完全对齐,说明计算单元的等待时间主要来自**计算本身**而非内存延迟,符合"计算瓶颈"的特征。 + +--- + +6. 与 Lab4 对比 + +| 指标 | Lab4(1D Block Tiling)| Lab5(2D Block Tiling)| 变化 | +|------|----------------------|----------------------|------| +| Compute (SM) Throughput | 53.54% | **74.99%** | ↑ 21.5% | +| Memory Throughput | 79.46% | 59.39% | ↓ 20.1% | +| L1/TEX Throughput | 79.79% | 62.64% | ↓ 17.2% | +| DRAM Throughput | 9.09% | **3.84%** | ↓ 5.3% | +| 瓶颈类型 | 访存瓶颈 | **计算瓶颈** | 质变 | +| 每线程寄存器数 | 46 | 123 | ↑ 167% | +| Block Size | 512 | 256 | ↓ 50% | +| Theoretical Occupancy | 50% | 25% | ↓ 25% | +| Achieved Occupancy | 49.38% | 23.76% | ↓ 25.6% | +| Waves Per SM | 18.96 | 4.74 | ↓ 75% | +| 执行时间(单次)| 14.56 ms | **10.61 ms** | **↓ 27.1%** | + +核心结论:2D Block Tiling 以**寄存器换性能**——每线程寄存器暴增 167%,Occupancy 腰斩至 25%,但算术强度大幅提升,Kernel 从访存瓶颈转为计算瓶颈,执行时间缩短约 **27%**。 + +--- + +7. 当前瓶颈与下一步优化方向 + +7.1 当前瓶颈:计算管线利用率 + +Compute Throughput 74.99% 距理论峰值仍有约 25% 的空间,根源是 **Occupancy 仅 25%**——每 SM 只有 2 个活跃 Block,无法充分隐藏指令延迟。Nsight 估计消除寄存器限制可带来高达 **75% 的本地加速**。 + +7.2 向量化访存 → float4 加载 + +对全局内存到共享内存的搬运阶段使用 `float4` 向量化加载,一条指令搬运 16 字节,减少指令数并提升内存带宽饱和度: + +```cpp +// 替换标量加载 +float4 tmp = reinterpret_cast(&A[row * K + col])[0]; +``` + +7.3 寄存器压力优化 → `__launch_bounds__` + +通过 `__launch_bounds__(256, 2)` 提示编译器目标 Block 数,引导寄存器分配策略,避免编译器过度分配导致 Occupancy 进一步降低: + +```cpp +__global__ __launch_bounds__(256, 2) +void sgemm2DBlocktiling(...) { ... } +``` + +7.4 异步拷贝 → `cp.async`(Ampere+) + +利用 A800 的 `cp.async` 指令将全局内存到共享内存的数据搬运与计算重叠(Software Pipelining),进一步隐藏剩余的内存延迟。 diff --git a/outputs/gpu-programming-course/code/labs/lab6.md b/outputs/gpu-programming-course/code/labs/lab6.md new file mode 100644 index 0000000..6396ddc --- /dev/null +++ b/outputs/gpu-programming-course/code/labs/lab6.md @@ -0,0 +1,181 @@ +# Lab6:SGEMM 向量化访存(sgemmVectorize)性能分析 + +## 实验目标 + +在 Lab5(2D Block Tiling)的基础上,对全局内存到共享内存的搬运阶段引入 **float4 向量化加载**——每条指令一次搬运 128 位(4 个 float),减少加载指令数量并降低每线程寄存器压力,从而在不改变 Tiling 结构的前提下进一步提升执行效率。 + +--- + +1. Kernel 启动配置 + +本次实验 Kernel 为 `sgemmVectorize`,启动参数与 Lab5 相同: + +| 参数 | 数值 | +|------|------| +| Grid Size | (32, 32, 1) = 1,024 个 Block | +| Block Size | 256 threads | +| 总线程数 | 262,144 | +| 每 Block 静态共享内存 | 8.19 KB | +| 每 Block 动态共享内存 | 0 | +| 每线程寄存器数 | **103**(Lab5 为 123,↓ 16%)| +| Waves Per SM | 4.74 | +| # SMs | 108 | + +向量化加载将每线程寄存器数从 Lab5 的 123 降至 **103**,但 Block Limit Registers 仍为 2,Occupancy 依然受寄存器约束,维持在 25%。 + + +![alt text](image-15.png) +[图1:Nsight Compute Summary —— 多次运行概览与负载不均衡警告](image1.png) + +--- + +2. 核心性能指标 + +| 指标 | 数值 | +|------|------| +| 执行时间(单次)| 9.89 ms | +| SM 频率 | 1.27 GHz | +| 总 Elapsed Cycles | 12,612,355 | +| SM Active Cycles | 11,937,078.94 | +| DRAM 频率 | 1.59 GHz | + +--- + +3. 吞吐量分析 + +| 指标 | 数值 | +|------|------| +| Compute (SM) Throughput | **79.64%** | +| Memory Throughput | 59.64% | +| L1/TEX Cache Throughput | 63.01% | +| L2 Cache Throughput | 10.05% | +| DRAM Throughput | 4.12% | + +Kernel 继续保持**计算瓶颈**状态(Compute > Memory),Nsight 再次给出 "High Compute Throughput" 警告。 + +与 Lab5 相比,Compute Throughput 从 74.99% 提升至 **79.64%**(+4.65%),说明向量化加载减少了加载指令数,让计算管线更加饱和。DRAM Throughput(4.12%)与 Lab5(3.84%)基本持平,全局内存访问仍被有效缓存。 + + +![alt text](image-16.png) +[图2:Nsight Compute Details —— GPU Speed of Light 与 Launch Statistics](image2.png) + +--- + +4. Occupancy 分析 + +| 指标 | 数值 | +|------|------| +| Theoretical Occupancy | 25% | +| Achieved Occupancy | 23.77% | +| Theoretical Active Warps per SM | 16 | +| Achieved Active Warps per SM | 15.21 | +| Block Limit Registers(绑定约束)| **2 blocks/SM** | +| Block Limit Shared Mem | 7 blocks/SM | +| Block Limit Warps | 8 blocks/SM | +| Block Limit SM | 32 blocks/SM | + +每线程 103 个寄存器 × 256 threads = 26,368 寄存器/Block,65536 ÷ 26,368 ≈ 2.48,向下取整仍为 **Block Limit Registers = 2**,因此 Occupancy 维持在与 Lab5 相同的 **25%**。 + +尽管寄存器从 123 降至 103,但尚未跨越"从 2 Block/SM 到 3 Block/SM"的阈值(需降至 ≤85 寄存器/线程),Occupancy 瓶颈暂未解除。 + +--- + +5. 新瓶颈:SM 负载不均衡 + + +![alt text](image-18.png) +[图3:Nsight Compute Details —— Occupancy 分析、工作负载分布与负载不均衡警告](image3.png) + +Lab6 首次出现了 Lab4/5 中没有的负载不均衡警告: + +| 警告 | Est. Speedup | +|------|-------------| +| SMs Workload Imbalance | 5.02% | +| SMSPs Workload Imbalance | 5.01% | +| L1 Slices Workload Imbalance | 5.02% | + +Nsight 描述:部分 SM 的 Active Cycles 远高于平均值,最大偏差为均值的 +5.30%,最小偏差为 −5.05%。 + +**根本原因:Wave 尾效应(Tail Effect)** + +- A800 共 108 个 SM,每 SM 最多 2 个 Block(寄存器限制),每波最多消化 216 个 Block +- Grid 共 1,024 个 Block,需要 ⌈1024 ÷ 216⌉ = **5 波**(Waves Per SM = 4.74) +- 前 4 波:864 个 Block,每 SM 均获得 8 个 Block(均匀) +- 第 5 波(尾波):仅剩 **160 个 Block**,分配到 108 个 SM 中 + - 80 个 SM 获得 2 个 Block(满载) + - 28 个 SM 获得 0 个 Block(空转等待) +- 满载 SM 与空载 SM 并存,造成约 **5% 的执行时间浪费** + +--- + +6. GPU 与内存工作负载分布 + +| 指标 | 数值 | +|------|------| +| Average SM Active Cycles | 11,937,078.94 | +| Average L1 Active Cycles | 11,937,078.94 | +| Average L2 Active Cycles | 9,717,145.43 | +| Average DRAM Active Cycles | 648,740.40 | +| Total SM Elapsed Cycles | 1,362,046,546 | +| Total L2 Elapsed Cycles | 961,225,440 | +| Total SMSP Elapsed Cycles | 5,448,186,184 | +| Total DRAM Elapsed Cycles | 630,370,304 | + +DRAM Active Cycles(648,740)与 Lab5(649,232)几乎相同,说明向量化并未改变全局内存的访问总量,只是减少了指令数与寄存器占用。 + +--- + +7. 与 Lab5 对比 + +| 指标 | Lab5(2D Block Tiling)| Lab6(向量化访存)| 变化 | +|------|----------------------|-----------------|------| +| Compute (SM) Throughput | 74.99% | **79.64%** | ↑ 4.65% | +| Memory Throughput | 59.39% | 59.64% | ≈ 不变 | +| L1/TEX Throughput | 62.64% | 63.01% | ≈ 不变 | +| DRAM Throughput | 3.84% | 4.12% | ≈ 不变 | +| 每线程寄存器数 | 123 | **103** | ↓ 16% | +| Theoretical Occupancy | 25% | 25% | 不变 | +| Achieved Occupancy | 23.76% | 23.77% | ≈ 不变 | +| 主要警告 | Theoretical Occupancy | **SM Workload Imbalance** | 瓶颈转移 | +| 执行时间(单次)| 10.61 ms | **9.89 ms** | **↓ 6.8%** | + +核心结论:向量化访存通过减少加载指令数,使 Compute Throughput 提升了约 5%,执行时间缩短 **6.8%**。占用率瓶颈未变,但原有的 Theoretical Occupancy 警告消退,新的主要优化点变为 **SM 负载不均衡**(尾波效应)。 + +--- + +8. 下一步优化方向 + +8.1 消除 Wave 尾效应 → 调整 Grid 为 108 的倍数 + +使 Grid 大小为 `(108 × 2) = 216` 的整数倍,消除不完整的尾波: + +```cpp +// 当前:grid = (N/BN) × (M/BM) = 32×32 = 1024(非 216 的整数倍) +// 优化:若矩阵规模允许,调整为 1080(216×5)或使用 Persistent Kernel +dim3 grid(ceil(N / BN), ceil(M / BM)); +``` + +8.2 继续降低寄存器压力 → 突破 Block Limit 阈值 + +当前 103 寄存器/线程距离"3 Block/SM"阈值(≤85)仍有差距。可尝试: + +- 使用 `__launch_bounds__(256, 3)` 强制编译器将目标占用率提升至 75% +- 减小 TM 或 TN,降低累加寄存器数量 + +8.3 异步流水线 → `cp.async` 双缓冲(Double Buffering) + +利用 A800(Ampere 架构)的 `cp.async` 指令,将当前 tile 的计算与下一个 tile 的数据预取重叠,将剩余的内存延迟彻底隐藏: + +```cpp +// 使用 __pipeline_memcpy_async 将全局→共享内存的拷贝异步化 +__pipeline_memcpy_async(&smemA[...], &A[...], sizeof(float4)); +__pipeline_commit(); +// 计算当前 tile ... +__pipeline_wait_prior(0); // 等待下一 tile 数据就绪 +``` diff --git a/outputs/gpu-programming-course/code/labs/lab7.md b/outputs/gpu-programming-course/code/labs/lab7.md new file mode 100644 index 0000000..0c5a0ce --- /dev/null +++ b/outputs/gpu-programming-course/code/labs/lab7.md @@ -0,0 +1,185 @@ +# Lab7:SGEMM 自动调优(sgemmAutotuned)性能分析 + +## 实验目标 + +在 Lab6(向量化访存)的基础上,通过**自动调优(Autotuning)**对 Tiling 参数(BK、BM、BN、TM、TN)进行搜索,找到在当前 GPU 硬件上共享内存利用率与寄存器压力最优的配置组合,突破之前版本的性能瓶颈。 + +--- + +1. Kernel 启动配置 + +本次实验 Kernel 为 `sgemmAutotuned`,核心变化是**共享内存用量翻倍**: + +| 参数 | Lab6(向量化)| Lab7(自动调优)| 变化 | +|------|-------------|--------------|------| +| Grid Size | (32, 32, 1) | (32, 32, 1) | 不变 | +| Block Size | 256 | 256 | 不变 | +| 总线程数 | 262,144 | 262,144 | 不变 | +| 每 Block 静态共享内存 | 8.19 KB | **16.38 KB** | ↑ 2× | +| 每线程寄存器数 | 103 | **102** | ↓ 1 | +| Waves Per SM | 4.74 | 4.74 | 不变 | +| # SMs | 108 | 108 | 不变 | +| 共享内存配置总量/SM | 65.54 KB | **102.40 KB** | ↑ 56% | + +自动调优将 BK(K 方向分块大小)翻倍,使每次 tile 迭代能加载更多数据到共享内存,显著提升每次访存的**计算复用率**。 + + +![alt text](image-19.png) +[图1:Nsight Compute Summary —— 多次运行概览,SM 负载不均衡警告消失](image1.png) + +--- + +2. 核心性能指标 + +| 指标 | 数值 | +|------|------| +| 执行时间(单次)| 9.54 ms | +| SM 频率 | 1.27 GHz | +| 总 Elapsed Cycles | 12,156,728 | +| SM Active Cycles | 11,525,760.90 | +| DRAM 频率 | 1.59 GHz | + +--- + +3. 吞吐量分析 + +| 指标 | 数值 | +|------|------| +| Compute (SM) Throughput | **82.32%** | +| Memory Throughput | 61.67% | +| L1/TEX Cache Throughput | 65.04% | +| L2 Cache Throughput | 7.36% | +| DRAM Throughput | 4.28% | + +**里程碑:Compute Throughput 首次突破 80%。** + +Nsight 的警告由 Lab6 的 "High Compute Throughput" 升级为更严格的 **"High Throughput"**: + +> The kernel is utilizing greater than 80.0% of the available compute or memory performance of the device. To further improve performance, work will likely need to be shifted from the most utilized to another unit. + +这意味着 Kernel 已逼近 A800 的硬件峰值,单纯在当前算法结构内优化空间已十分有限,后续提升需要从计算管线结构本身入手。 + +与 Lab6 对比: +- Compute 从 79.64% 提升至 **82.32%**(+2.68%) +- DRAM 从 4.12% 略升至 4.28%,全局内存仍被高效缓存 + + +![alt text](image-20.png) +[图2:Nsight Compute Details —— GPU Speed of Light 与 Launch Statistics](image2.png) + +--- + +4. Occupancy 分析 + +| 指标 | 数值 | +|------|------| +| Theoretical Occupancy | 25% | +| Achieved Occupancy | 23.74% | +| Theoretical Active Warps per SM | 16 | +| Achieved Active Warps per SM | 15.21 | +| Block Limit Registers(绑定约束)| **2 blocks/SM** | +| Block Limit Shared Mem | **5 blocks/SM**(Lab6 为 7)| +| Block Limit Warps | 8 blocks/SM | +| Block Limit SM | 32 blocks/SM | + +共享内存从 8.19 KB 翻倍至 16.38 KB 后,Block Limit Shared Mem 从 7 降至 **5**: + +- A800 每 SM 共享内存为 164 KB(含驱动保留) +- 可用共享内存 ≈ 102.40 KB(配置值),16.38 KB × 5 = 81.9 KB ≈ 可用量上限 +- 但寄存器约束(Block Limit Registers = 2)仍更严格,依旧是**绑定约束** + +因此 Occupancy 维持在 25%,自动调优的提升来自**提高每个 Block 内的计算密度**,而非增加并发 Block 数。 + +--- + +5. SM 负载不均衡问题的消除 + +Lab6 中 SM Workload Imbalance 估计可带来 5.02% 加速,而 Lab7 中该警告**完全消失**(Summary 页所有行的 Estimated Speedup 均为 0.00%)。 + +Waves Per SM 仍为 4.74(Grid 仍为 1024 blocks,每 SM 仍限 2 blocks),理论上尾波仍然存在。不均衡消失的原因在于:共享内存翻倍后每个 Block 的**计算量增加**,尾波中负载较少的 SM 处理 4 个 Block 所用时间与满载 SM 处理 5 个 Block 的时间差缩小,不均衡程度降至 Nsight 警告阈值以下。 + + +![alt text](image-21.png) +[图3:Nsight Compute Details —— Occupancy 分析与 GPU/内存工作负载分布](image3.png) + +--- + +6. GPU 与内存工作负载分布 + +| 指标 | 数值 | +|------|------| +| Average SM Active Cycles | 11,525,760.90 | +| Average L1 Active Cycles | 11,525,760.90 | +| Average L2 Active Cycles | 6,628,348 | +| Average DRAM Active Cycles | 649,898.90 | +| Total SM Elapsed Cycles | 1,312,706,470 | +| Total L2 Elapsed Cycles | 926,294,080 | +| Total SMSP Elapsed Cycles | 5,250,825,880 | +| Total DRAM Elapsed Cycles | 607,620,224 | + +Average L2 Active Cycles(6,628,348)比 Lab6(9,717,145)下降约 **32%**,说明更大的共享内存 tile 进一步减少了 L2 的访问频率,数据复用率更高。DRAM Active Cycles 与 Lab6 几乎相同,全局内存瓶颈已被彻底消除。 + +--- + +7. 与 Lab6 对比 + +| 指标 | Lab6(向量化)| Lab7(自动调优)| 变化 | +|------|-------------|--------------|------| +| Compute (SM) Throughput | 79.64% | **82.32%** | ↑ 2.68% | +| Memory Throughput | 59.64% | 61.67% | ↑ 2.0% | +| L1/TEX Throughput | 63.01% | 65.04% | ↑ 2.0% | +| L2 Throughput | 10.05% | **7.36%** | ↓ 2.7% | +| DRAM Throughput | 4.12% | 4.28% | ≈ 不变 | +| 静态共享内存/Block | 8.19 KB | **16.38 KB** | ↑ 2× | +| 每线程寄存器数 | 103 | 102 | ↓ 1 | +| Theoretical Occupancy | 25% | 25% | 不变 | +| Achieved Occupancy | 23.77% | 23.74% | ≈ 不变 | +| Block Limit Shared Mem | 7 | 5 | ↓ 2 | +| SM 负载不均衡警告 | **有(5.02%)** | **无(0.00%)** | 消除 | +| Nsight 性能警告等级 | High Compute Throughput | **High Throughput (>80%)** | 升级 | +| 执行时间(单次)| 9.89 ms | **9.54 ms** | **↓ 3.5%** | + +核心结论:自动调优通过将共享内存 tile 翻倍,消除了 SM 负载不均衡,将 Compute Throughput 推过 **80% 大关**,执行时间再缩短 3.5%。当前版本已接近 A800 的硬件峰值,寄存器约束(Occupancy 25%)是唯一剩余的结构性瓶颈。 + +--- + +8. 当前瓶颈与下一步优化方向 + +8.1 当前瓶颈:寄存器限制的低 Occupancy + +Nsight 提示 Theoretical Occupancy 仍仅 25%,Est. Local Speedup 达 **75%**——这是当前最大的单项潜在收益。根本原因是每线程 102 个寄存器,使每 SM 最多只能运行 2 个 Block。 + +8.2 异步流水线 → `cp.async` 双缓冲 + +利用 Ampere 架构的 `cp.async` 指令实现 Software Pipelining:在计算当前 tile 的同时,异步预取下一个 tile 到备用共享内存缓冲区,将剩余的内存延迟与计算完全重叠: + +```cpp +// 双缓冲:ping-pong 两组共享内存 +__shared__ float smemA[2][BK][BM]; +__shared__ float smemB[2][BK][BN]; + +// 预取第一个 tile +__pipeline_memcpy_async(&smemA[0][...], &A[...], sizeof(float4)); +__pipeline_commit(); + +for (int tile = 0; tile < K / BK; ++tile) { + __pipeline_wait_prior(0); // 等待当前 tile 就绪 + __syncthreads(); + // 异步预取下一个 tile 到另一缓冲 + __pipeline_memcpy_async(&smemA[(tile+1)%2][...], ...); + __pipeline_commit(); + // 计算当前 tile ... + __syncthreads(); +} +``` + +8.3 寄存器压力 → `__launch_bounds__` 或减小 TM/TN + +若能将每线程寄存器降至 ≤85,Block Limit Registers 将从 2 提升至 3,Occupancy 从 25% 跃升至 ~37.5%,理论上可再带来约 50% 的延迟隐藏改善。 diff --git a/outputs/gpu-programming-course/code/labs/lab8.md b/outputs/gpu-programming-course/code/labs/lab8.md new file mode 100644 index 0000000..11ed604 --- /dev/null +++ b/outputs/gpu-programming-course/code/labs/lab8.md @@ -0,0 +1,183 @@ +# Lab8:SGEMM Warp Tiling 性能分析 + +## 实验目标 + +在 Lab7(自动调优 2D Block Tiling)的基础上,引入 **Warp-level Tiling**——将 Block 内的线程按 Warp 划分子任务,每个 Warp(32 个线程)独立负责输出矩阵中一个更小的子块,使得同一 Warp 内的线程共享寄存器中的数据,减少共享内存访问次数,进一步提升计算密度。 + +--- + +1. Kernel 启动配置 + +本次实验 Kernel 为 `sgemmWarptiling`,Block Size 从 256 减半至 128,但每线程寄存器数大幅增加: + +| 参数 | Lab7(自动调优)| Lab8(Warp Tiling)| 变化 | +|------|--------------|------------------|------| +| Grid Size | (32, 32, 1) | (32, 32, 1) | 不变 | +| Block Size | 256 | **128** | ↓ 50% | +| 总线程数 | 262,144 | 131,072 | ↓ 50% | +| 每 Block 静态共享内存 | 16.38 KB | 16.38 KB | 不变 | +| 每线程寄存器数 | 102 | **167** | ↑ 64% | +| Waves Per SM | 4.74 | **3.16** | ↓ 33% | +| 每 Block Warp 数 | 8 warps | **4 warps** | ↓ 50% | +| # SMs | 108 | 108 | 不变 | + +Block Size 减半使每 Block 总寄存器需求从 26,112(102×256)降至 21,376(167×128),即便每线程寄存器大幅增加,每 Block 的寄存器占用反而**减少了 18%**,这是突破寄存器限制的关键。 + + +![alt text](image-22.png) +[图1:Nsight Compute Summary —— 多次运行概览,Compute Throughput 突破 87%](image1.png) + +--- + +2. 核心性能指标 + +| 指标 | 数值 | +|------|------| +| 执行时间(单次)| 8.94 ms | +| SM 频率 | 1.27 GHz | +| 总 Elapsed Cycles | 11,390,484 | +| SM Active Cycles | 10,804,015.39 | +| DRAM 频率 | 1.59 GHz | + +--- + +3. 吞吐量分析 + +| 指标 | 数值 | +|------|------| +| Compute (SM) Throughput | **87.88%** | +| Memory Throughput | **38.52%** | +| L1/TEX Cache Throughput | 40.61% | +| L2 Cache Throughput | 7.85% | +| DRAM Throughput | 4.55% | + +两个最显著的变化: + +**① Compute Throughput 达到历史最高 87.88%**,仍为 "High Throughput (>80%)" 状态,距离硬件峰值仅剩约 12%。 + +**② Memory Throughput 骤降至 38.52%**(Lab7 为 61.67%,**↓ 23 个百分点**)。L1/TEX 从 65.04% 降至 40.61%,降幅同样显著。这表明 Warp Tiling 通过让 Warp 内线程在**寄存器层**复用数据,大幅减少了对共享内存的访问次数,共享内存不再是主要压力源。 + +Compute/Memory 吞吐量之比(87.88% / 38.52% ≈ **2.28**)是历次实验中最高,说明每单位内存访问对应的计算量是所有版本中最多的。 + + +![alt text](image-23.png) +[图2:Nsight Compute Details —— GPU Speed of Light 与 Launch Statistics](image2.png) + +--- + +4. Occupancy 分析 + +| 指标 | 数值 | +|------|------| +| Theoretical Occupancy | **18.75%** | +| Achieved Occupancy | 16.88% | +| Theoretical Active Warps per SM | 12 | +| Achieved Active Warps per SM | 10.80 | +| Block Limit Registers(绑定约束)| **3 blocks/SM** | +| Block Limit Shared Mem | 5 blocks/SM | +| Block Limit Warps | 16 blocks/SM | +| Block Limit SM | 32 blocks/SM | + +Block Limit Registers 从 2 提升至 3,是本实验最关键的结构性变化: + +- 每 Block 寄存器需求:167 × 128 = **21,376** +- 65,536 ÷ 21,376 ≈ **3.06**,向下取整得 Block Limit Registers = **3** +- 每 SM 可同时运行 3 个 Block = 3 × 4 warps = **12 active warps** +- 理论占用率:12 / 64 = **18.75%** + +看似 Occupancy 从 Lab7 的 25% 反而降低至 18.75%,实为**以更少 Warp 承载更多计算**的权衡: +- Lab7:每 SM 2 Block × 8 warps = 16 warps,每 warp 计算量较小 +- Lab8:每 SM 3 Block × 4 warps = 12 warps,每 warp 独立完成更大子块的计算 + +Warp 数虽减少,但每个 Warp 通过寄存器内的 TM×TN 累加阵列完成更多乘加运算,**算术强度提升抵消了并行度下降**,执行时间依然缩短。 + +Nsight 指出 Theoretical Occupancy 的潜在加速高达 **81.25%**,但这一数字仅为上界估计,实际中寄存器限制与指令级并行的权衡使之难以完全兑现。 + + +![alt text](image-24.png) +[图3:Nsight Compute Details —— Occupancy 分析与 GPU/内存工作负载分布](image3.png) + +--- + +5. GPU 与内存工作负载分布 + +| 指标 | 数值 | +|------|------| +| Average SM Active Cycles | 10,804,015.39 | +| Average L1 Active Cycles | 10,804,015.39 | +| Average L2 Active Cycles | 7,939,393.11 | +| Average DRAM Active Cycles | 647,814.60 | +| Total SM Elapsed Cycles | 1,230,186,466 | +| Total L2 Elapsed Cycles | 867,887,760 | +| Total SMSP Elapsed Cycles | 4,920,745,864 | +| Total DRAM Elapsed Cycles | 569,370,624 | + +DRAM Active Cycles(647,814)与前几版基本持平,全局内存访问总量不变,说明优化均发生在**计算侧**,而非进一步减少全局内存访问。 + +--- + +6. 与 Lab7 对比 + +| 指标 | Lab7(自动调优)| Lab8(Warp Tiling)| 变化 | +|------|--------------|------------------|------| +| Compute (SM) Throughput | 82.32% | **87.88%** | ↑ 5.56% | +| Memory Throughput | 61.67% | **38.52%** | ↓ 23.2% | +| L1/TEX Throughput | 65.04% | **40.61%** | ↓ 24.4% | +| L2 Throughput | 7.36% | 7.85% | ≈ 不变 | +| DRAM Throughput | 4.28% | 4.55% | ≈ 不变 | +| 每线程寄存器数 | 102 | 167 | ↑ 64% | +| Block Size | 256 | **128** | ↓ 50% | +| Block Limit Registers | 2 | **3** | ↑ 1 | +| Theoretical Occupancy | 25% | 18.75% | ↓ 6.25% | +| Achieved Occupancy | 23.74% | 16.88% | ↓ 6.86% | +| Waves Per SM | 4.74 | **3.16** | ↓ 33% | +| 执行时间(单次)| 9.54 ms | **8.94 ms** | **↓ 6.3%** | + +核心结论:Warp Tiling 通过将 Block Size 减半并大幅增加每线程寄存器(102→167),使 Block Limit Registers 从 2 提升至 3,打破了连续多版实验中"每 SM 最多 2 个 Block"的瓶颈。Memory Throughput 下降 23 个百分点,说明 Warp 内的寄存器级数据复用已极大减少了共享内存压力,执行时间再缩短 **6.3%**。 + +--- + +7. 当前瓶颈与下一步优化方向 + +7.1 当前瓶颈:Occupancy 降至历史最低(18.75%) + +每 SM 仅 12 个 active warps,隐藏内存/指令延迟的能力继续下降。Nsight 的 81.25% Est. Local Speedup 是历次实验中最高的警告值,但这是硬件结构上的根本限制,需要在算法层面引入流水线才能绕过。 + +7.2 异步流水线 → `cp.async` 双缓冲 + +在 Warp Tiling 的基础上引入双缓冲,让每个 Warp 在计算当前 tile 时,异步预取下一个 tile 数据,彻底隐藏共享内存填充延迟: + +```cpp +// 双缓冲 ping-pong +__shared__ float smemA[2][BK * BM]; +__shared__ float smemB[2][BK * BN]; + +int buf = 0; +// 预取第一个 tile(异步) +__pipeline_memcpy_async(&smemA[buf][...], &A[...], sizeof(float4)); +__pipeline_commit(); + +for (int tile = 0; tile < K / BK; ++tile) { + int next = 1 - buf; + // 异步预取下一 tile + __pipeline_memcpy_async(&smemA[next][...], &A[next_tile_offset], sizeof(float4)); + __pipeline_commit(); + __pipeline_wait_prior(1); // 等待当前 tile 就绪 + __syncthreads(); + // 用 smemA[buf] / smemB[buf] 计算当前 tile + warp_level_mma(...); + buf = next; + __syncthreads(); +} +``` + +7.3 Tensor Core → `wmma` / `mma` PTX 指令 + +A800 支持 Tensor Core(Ampere 架构 tf32/fp16 MMA),每个 Tensor Core 时钟周期可完成 16×16×16 矩阵乘加,吞吐量是普通 CUDA Core 的 8× 以上。切换至 Tensor Core 可在不改变内存访问结构的情况下,将 Compute Throughput 的利用率大幅提升。 diff --git "a/outputs/gpu-programming-course/docs/advanced-chapter1/\347\254\25412\347\253\240 Thread Block Clusters\344\270\216\345\210\206\345\270\203\345\274\217\345\205\261\344\272\253\345\206\205\345\255\230.md" "b/outputs/gpu-programming-course/docs/advanced-chapter1/\347\254\25412\347\253\240 Thread Block Clusters\344\270\216\345\210\206\345\270\203\345\274\217\345\205\261\344\272\253\345\206\205\345\255\230.md" index 0e90729..4837326 100644 --- "a/outputs/gpu-programming-course/docs/advanced-chapter1/\347\254\25412\347\253\240 Thread Block Clusters\344\270\216\345\210\206\345\270\203\345\274\217\345\205\261\344\272\253\345\206\205\345\255\230.md" +++ "b/outputs/gpu-programming-course/docs/advanced-chapter1/\347\254\25412\347\253\240 Thread Block Clusters\344\270\216\345\210\206\345\270\203\345\274\217\345\205\261\344\272\253\345\206\205\345\255\230.md" @@ -229,7 +229,7 @@ CUDA Programming Guide 对分布式共享内存的定义: 分布式共享内存的关键特性: -1. 统一地址空间:簇中所有线程块的共享内存组成一个连续的分布式地址空间; +1. 统一地址空间:簇中所有线程块的共享内存在逻辑上组成统一的分布式地址空间,但各块的共享内存在物理上是独立的。访问远程线程块的共享内存必须通过 `map_shared_rank()` 进行地址映射,不能简单地做线性偏移寻址。 2. 总大小 = 每个线程块的共享内存大小 × 簇中线程块的数量; 3. 完全可访问:任何线程块可以读、写、或在分布式共享内存的任何地址上执行原子操作,无论该地址属于本地线程块还是远程线程块; 4. 每块共享内存大小不变:无论是否使用分布式共享内存,静态或动态共享内存的大小规格仍然是每线程块的。 @@ -377,7 +377,7 @@ __global__ void clusterHist_kernel(int *bins, const int nbins, // 根据直方图 bin 数量动态决定簇大小 { cudaLaunchConfig_t config = {0}; - config.gridDim = array_size / threads_per_block; + config.gridDim = dim3((array_size + thr`eads_per_block - 1) / threads_per_block); config.blockDim = threads_per_block; // 簇大小取决于直方图 bin 数量 @@ -435,7 +435,7 @@ __global__ void clusterHist_kernel(int *bins, const int nbins, |------|---------|------------|---------| | 共享内存 | 受限于单块SMEM(~228KB) | 低(SMEM原子) | bin数量少 | | 全局内存 | 几乎无限(HBM) | 高(GMEM原子) | bin数量多 | -| 分布式共享内存 | 中等(N×SMEM每块,N≤8) | 低(SMEM原子) | bin数量适中 | +| 分布式共享内存 | 中等(N×SMEM每块,N≤8) | 中(远程SMEM原子) | bin数量适中 | 当 bin 数量超出单块共享内存但不超过 N 倍单块共享内存(N≤8)时,分布式共享内存方案可以同时享受大容量和低延迟的优势。 @@ -471,12 +471,11 @@ __global__ void distributed_reduce(float *data, float *result, int N) int block_rank = cluster.block_rank(); // 1. 每个块加载属于它的数据段到共享内存 - int elements_per_block = N / (gridDim.x); + int elements_per_block = N / gridDim.x; int start = block_id * elements_per_block; - int local_size = elements_per_block / blockDim.x; float local_sum = 0.0f; - for (int i = tid; i < local_size; i += blockDim.x) { + for (int i = tid; i < elements_per_block; i += blockDim.x) { local_sum += data[start + i]; } @@ -605,7 +604,7 @@ CUDA Programming Guide 中 Cluster Group 提供的完整 API 如下表所示。 ## 12.12 硬件限制与注意事项 -### 12.8.1 网格维度约束 +### 12.12.1 网格维度约束 在使用 Thread Block Cluster 时,有一个关键的约束: @@ -613,7 +612,7 @@ CUDA Programming Guide 中 Cluster Group 提供的完整 API 如下表所示。 例如,如果簇在 X 方向大小为 2,那么启动核函数时 `numBlocks.x`(也即 `gridDim.x`)必须能被 2 整除。这个约束确保了所有簇都是"完整的"——不会出现一个簇只有部分线程块被启动的情况。 -### 12.8.2 gridDim 的语义 +### 12.12.2 gridDim 的语义 如前所述,`gridDim` 仍然表示线程块的数量(而非簇的数量),这是为了兼容性考虑。CUDA Programming Guide 明确: @@ -621,7 +620,7 @@ CUDA Programming Guide 中 Cluster Group 提供的完整 API 如下表所示。 如果你需要知道当前的簇排名或簇网格维度,应使用 Cluster Group API 提供的 `block_rank()`、`block_index()`、`dim_blocks()` 等函数。 -### 12.8.3 最大簇大小与 MIG +### 12.12.3 最大簇大小与 MIG 可以同时被 GPC 调度的线程块数量受限于 GPC 中的 SM 数量。在以下场景中最大簇大小会受到限制: @@ -630,7 +629,7 @@ CUDA Programming Guide 中 Cluster Group 提供的完整 API 如下表所示。 建议通过 `cudaOccupancyMaxPotentialClusterSize` API 查询实际可用的最大簇大小,而不是硬编码为 8。 -### 12.8.4 共享内存限制 +### 12.12.4 共享内存限制 在使用分布式共享内存时,共享内存的限制仍然是每线程块的。每个线程块最多可寻址的共享内存容量受硬件限制(Hopper 上为 227KB)。分布式共享内存的总大小是每块大小乘以簇中块数,但你不能在单个块中访问超出其自身共享内存容量限制的地址。 @@ -730,7 +729,7 @@ cluster_warp_specialized(float *input, float *output, int N) Thread Block Cluster 的实现建立在 Cooperative Groups 框架之上。理解两者的关系有助于更深入地理解这一特性。 -### 12.13.1 cooperative_groups 中的 Cluster Group 实现 +### 12.14.1 cooperative_groups 中的 Cluster Group 实现 当你在核函数中调用 `cooperative_groups::this_cluster()` 时: @@ -760,19 +759,24 @@ namespace cooperative_groups { } ``` -### 12.13.2 cluster.sync() 的内部实现 +### 12.14.2 cluster.sync() 的内部实现 -`cluster.sync()` 是硬件支持的同步操作。在 PTX 层面,它使用 `barrier.cluster` 指令。整个流程: +`cluster.sync()` 是硬件支持的簇级同步操作,依赖于 Hopper 架构的 Cluster Barrier 机制(PTX 中通常涉及` barrier.cluster `相关指令,具体实现由编译器决定)。 -1. 每个线程块中的线程首先执行块内同步(`__syncthreads()`); -2. 然后通过 GPC 上的硬件 barrier 进行跨块同步; -3. 所有块到达后,barrier 翻转,所有块继续执行。 +`cluster.sync()` 是一个块级集体操作(collective operation),要求块内所有线程都必须到达该调用点。它本身已隐式完成块内同步,用户不应在 `cluster.sync() `前后额外调用` __syncthreads()`。 -这意味着 `cluster.sync()` 的开销: -- 最低开销:每个块内的一次 `__syncthreads()` + 一次硬件 barrier 操作; -- 实际的延迟取决于簇的大小和 GPC 上块的分布。 +执行流程: -### 12.13.3 不能用 cluster.sync() 做什么 +块内所有线程到达 `cluster.sync()` 后,硬件自动完成块内同步; + +各块的代表通过 GPC 上的硬件 barrier 进行跨块同步; + +所有块到达后,barrier 翻转,所有块继续执行。 + +`cluster.sync()` 的开销取决于簇大小和块在 GPC 上的分布,实际延迟由硬件 barrier 同步主导。 + + +### 12.14.3 不能用 cluster.sync() 做什么 虽然 `cluster.sync()` 提供了簇级别的同步,但它不能用于: @@ -780,9 +784,9 @@ namespace cooperative_groups { 2. 网格级同步:如果需要整个网格的同步,需要使用 Cooperative Groups 的 `grid.sync()`(有更多限制); 3. 细粒度 Warp 同步:`cluster.sync()` 是块级别的,不适用于 warp 级同步。 -## 12.14 实践建议与最佳实践 +## 12.15 实践建议与最佳实践 -### 12.14.1 何时使用 Thread Block Cluster +### 12.15.1 何时使用 Thread Block Cluster Thread Block Cluster 和分布式共享内存不是银弹。只有在以下场景中才值得使用: @@ -790,13 +794,13 @@ Thread Block Cluster 和分布式共享内存不是银弹。只有在以下场 2. 需要跨块细粒度协作:算法天然需要块之间交换数据; 3. 可以减少全局内存原子操作:通过 DSM 将跨块操作保留在共享内存域内。 -### 12.14.2 何时不应使用 +### 12.15.2 何时不应使用 1. 数据完全独立:每个块独立处理自己的数据,不需要块间通信; 2. 单块共享内存已足够:如果数据大小适合单块共享内存,使用传统方法更简单; 3. CC < 9.0 的硬件:Cluster 是 Hopper+ 独占特性。 -### 12.14.3 调试分布式共享内存的常见问题 +### 12.15.3 调试分布式共享内存的常见问题 1. 忘记 cluster.sync():这是最常见的错误。在访问 DSM 之前和退出之前都必须同步。 @@ -808,7 +812,7 @@ Thread Block Cluster 和分布式共享内存不是银弹。只有在以下场 5. 共享内存分配不足:动态共享内存大小仍然是每块的。如果每块需要 X 字节共享内存,而簇大小是 4,总分布式共享内存大小是 4X——但启动时只需指定 X。 -### 12.14.4 迁移现有代码到 Cluster 的步骤 +### 12.15.4 迁移现有代码到 Cluster 的步骤 1. 评估收益:确定算法是否真正能从跨块共享内存中受益; 2. 添加条件编译:使用 `#if __CUDA_ARCH__ >= 900` 保护 Cluster 代码; @@ -817,7 +821,8 @@ Thread Block Cluster 和分布式共享内存不是银弹。只有在以下场 5. 测试回退路径:确保在旧硬件上回退路径正确工作; 6. 性能对比:使用 Nsight Compute 对比新旧方案的性能。 -## 12.15 动手体验:扩展练习——分布式矩阵乘法预处理 +## 12.16 动手体验 +### 12.16.1 扩展练习——分布式矩阵乘法预处理 以下是一个扩展练习,展示如何使用 DSM 进行矩阵乘法的预处理步骤(数据重组/格式转换)。这个例子展示了一个实际场景:在 GEMM 的分块预处理中,使用 DSM 在簇内协同重组数据布局。 @@ -876,12 +881,12 @@ void launch_reorder(float *d_A, float *d_B, int M, int N, int tile_size) int tiles = M / tile_size; int cluster_x = 2, cluster_y = 2; + int rows_per_block = tile_size / (cluster_x * cluster_y); cudaLaunchConfig_t config = {0}; config.gridDim = dim3(tiles); config.blockDim = threads; - config.dynamicSmemBytes = tile_size * N * sizeof(float); - + config.dynamicSmemBytes = (tile_size / (cluster_x * cluster_y)) * N * sizeof(float); cudaLaunchAttribute attrs[1]; attrs[0].id = cudaLaunchAttributeClusterDimension; attrs[0].val.clusterDim.x = cluster_x; @@ -896,7 +901,7 @@ void launch_reorder(float *d_A, float *d_B, int M, int N, int tile_size) } ``` -## 12.9 动手体验:完整的分布式直方图程序 +### 12.16.2动手体验:完整的分布式直方图程序 下面是一个完整的、可编译的分布式直方图示例程序。它包含了主机端准备数据、动态选择簇大小、核函数执行和结果验证的全流程。 @@ -1051,9 +1056,9 @@ int main() } ``` -## 12.16 常见问题与故障排除 +## 12.17 常见问题与故障排除 -### 12.16.1 启动失败:网格维度不是簇大小的整数倍 +### 12.17.1 启动失败:网格维度不是簇大小的整数倍 症状:`cudaLaunchKernelEx` 返回 `cudaErrorInvalidConfiguration`。 @@ -1067,7 +1072,7 @@ int cluster_x = 4; blocks_x = ((blocks_x + cluster_x - 1) / cluster_x) * cluster_x; // 向上取整 ``` -### 12.16.2 DSM 访问返回垃圾值 +### 12.17.2 DSM 访问返回垃圾值 症状:从远程块的共享内存读取到未初始化或错误的数据。 @@ -1078,7 +1083,7 @@ blocks_x = ((blocks_x + cluster_x - 1) / cluster_x) * cluster_x; // 向上取整 解决方法:仔细检查 `cluster.sync()` 的位置和 `map_shared_rank` 的参数。 -### 12.16.3 簇同步死锁 +### 12.17.3 簇同步死锁 症状:kernel 无限期挂起,不返回。 @@ -1101,7 +1106,7 @@ if (block_rank == 0) { } ``` -### 12.16.4 性能不如预期 +### 12.17.4 性能不如预期 可能原因和解决方案: 1. 簇大小太大:减少簇大小,测试不同配置; @@ -1109,13 +1114,13 @@ if (block_rank == 0) { 3. 同步开销:评估是否真的需要 DSM——如果单块共享内存够用,去掉 Cluster; 4. MIG 限制:检查是否在 MIG 实例上运行,MIG 减少可用 SM 数量。 -### 12.16.5 编译错误:__cluster_dims__ 与 __block_size__ 冲突 +### 12.17.5 编译错误:__cluster_dims__ 与 __block_size__ 冲突 症状:`error: "__block_size__" and "__cluster_dims__" cannot be used together` 解决:选择其中一个。通常使用 `__cluster_dims__` 更简洁,除非需要以簇数为单位启动。 -## 12.17 性能基准参考 +## 12.18 性能基准参考 以下是在 NVIDIA H100 上使用不同方案进行直方图计算的性能对比(512 bins, 1M elements): @@ -1134,103 +1139,6 @@ if (block_rank == 0) { 2. DSM 在 cluster_size=2 时比单块再加速 36%; 3. DSM 从 4 到 8 的收益递减(2.7x → 3.0x),这是因为较大的簇带来更多的同步开销。 -## 12.18 动手体验2:扩展练习——分布式矩阵乘法预处理 - -除了直方图和归约,DSM 还可以用于矩阵乘法分块中的数据重组。以下是一个扩展练习,展示如何使用 DSM 在 Cluster 内协作将 row-major 数据重组为 block-optimized 布局: - -```cuda -// distributed_matrix_reorder.cu -// 使用 DSM 进行矩阵分块数据重组 -__global__ void __cluster_dims__(2, 2, 1) -distributed_matrix_reorder( - const float *__restrict__ global_A, - float *__restrict__ global_B, - int M, int N, int tile_size) -{ - extern __shared__ float smem[]; - namespace cg = cooperative_groups; - cg::cluster_group cluster = cg::this_cluster(); - - int block_rank = cluster.block_rank(); - int cluster_size = cluster.dim_blocks().x * cluster.dim_blocks().y; - int rows_per_block = tile_size / cluster_size; - int my_start_row = block_rank * rows_per_block; - - // Step 1: 加载数据到本地共享内存 - int global_row_offset = blockIdx.x * tile_size + my_start_row; - for (int i = threadIdx.x; i < rows_per_block * N; i += blockDim.x) { - int r = i / N; - int c = i % N; - smem[r * N + c] = global_A[(global_row_offset + r) * N + c]; - } - __syncthreads(); - - // Step 2: DSM交叉拷贝——每个块将自己的数据分布到所有块 - cluster.sync(); - - for (int r = 0; r < rows_per_block; r++) { - for (int c = 0; c < N; c++) { - int dst_block = c % cluster_size; - int dst_offset = r * (N / cluster_size) + (c / cluster_size); - float *dst_smem = cluster.map_shared_rank(smem, dst_block); - dst_smem[dst_offset] = smem[r * N + c]; - } - } - - cluster.sync(); - - // Step 3: 写回全局内存 - for (int i = threadIdx.x; i < rows_per_block * (N / cluster_size); i += blockDim.x) { - int r = i / (N / cluster_size); - int c = i % (N / cluster_size); - int global_idx = (global_row_offset + r) * N + c; - global_B[global_idx] = smem[r * (N / cluster_size) + c]; - } -} - -// 主机端动态启动封装 -void launch_matrix_reorder(float *d_A, float *d_B, int M, int N, int tile_size) -{ - dim3 threads(256); - int tiles = M / tile_size; - int cluster_x = 2, cluster_y = 2; - - cudaLaunchConfig_t config = {0}; - config.gridDim = dim3(tiles); - config.blockDim = threads; - config.dynamicSmemBytes = tile_size * N * sizeof(float); - - CUDA_CHECK(cudaFuncSetAttribute( - (void *)distributed_matrix_reorder, - cudaFuncAttributeMaxDynamicSharedMemorySize, - config.dynamicSmemBytes)); - - cudaLaunchAttribute attrs[1]; - attrs[0].id = cudaLaunchAttributeClusterDimension; - attrs[0].val.clusterDim.x = cluster_x; - attrs[0].val.clusterDim.y = cluster_y; - attrs[0].val.clusterDim.z = 1; - - config.numAttrs = 1; - config.attrs = attrs; - - CUDA_CHECK(cudaLaunchKernelEx(&config, distributed_matrix_reorder, - d_A, d_B, M, N, tile_size)); -} -``` - -### 设计要点分析 - -这个分布式矩阵重组核函数的设计体现了几个关键决策: - -1. 分块策略:将 `tile_size` 行数据分配给 `cluster_size` 个块,每个块处理 `rows_per_block = tile_size / cluster_size` 行。这里要求 `tile_size` 能被 `cluster_size` 整除。 - -2. DSM 交叉拷贝:每个块将自己的行数据按列重新分布——将属于其他块的列数据通过 `map_shared_rank` 直接写入目标块的共享内存。这一步避免了通过全局内存中转。 - -3. 同步点:两个 `cluster.sync()` 确保:(a) 所有块的本地数据加载完成;(b) 所有交叉拷贝完成。 - -4. 复杂度:这个示例展示了 DSM 的强大之处——如果没有 DSM,这种跨块数据重组需要通过全局内存原子操作或额外的 kernel launch 来完成。 - ## 12.19 GPU 架构演进中的 Cluster 设计哲学 Thread Block Cluster 的引入不仅仅是增加了一个编程层次——它反映了 GPU 硬件架构的深层演进趋势。 @@ -1283,7 +1191,7 @@ NVIDIA Blackwell 架构(CC 10.0/12.0)在 Cluster 的基础上进一步引入 Thread Block Clusters 和分布式共享内存在 NVIDIA Hopper 架构上开启了 GPU 编程的新维度。它们使得跨线程块的细粒度协作成为可能,为许多以前需要全局内存回退的算法提供了更高效的选择。 -## 12.11 习题 +## 12.21 习题 1. 解释 Thread Block Cluster 的共调度保证为什么是实现 `cluster.sync()` 的前提条件。如果没有共调度保证会发生什么? @@ -1297,7 +1205,7 @@ Thread Block Clusters 和分布式共享内存在 NVIDIA Hopper 架构上开启 6. 为什么在使用分布式共享内存时,需要在退出前再次调用 `cluster.sync()`?如果不调用会发生什么情况? -## 12.12 参考文献 +## 12.22 参考文献 1. CUDA C++ Programming Guide 13.0, Section 5.2.1 "Thread Block Clusters" 2. CUDA C++ Programming Guide 13.0, Section 5.2.2 "Blocks as Clusters" diff --git "a/outputs/gpu-programming-course/docs/advanced-chapter15/\347\254\25415\347\253\240 CUDA Graphs\351\253\230\347\272\247\347\211\271\346\200\247.md" "b/outputs/gpu-programming-course/docs/advanced-chapter15/\347\254\25415\347\253\240 CUDA Graphs\351\253\230\347\272\247\347\211\271\346\200\247.md" index 6f25983..be764ea 100644 --- "a/outputs/gpu-programming-course/docs/advanced-chapter15/\347\254\25415\347\253\240 CUDA Graphs\351\253\230\347\272\247\347\211\271\346\200\247.md" +++ "b/outputs/gpu-programming-course/docs/advanced-chapter15/\347\254\25415\347\253\240 CUDA Graphs\351\253\230\347\272\247\347\211\271\346\200\247.md" @@ -151,6 +151,10 @@ __global__ void secondary_kernel() cudaLaunchAttribute attribute[1]; attribute[0].id = cudaLaunchAttributeProgrammaticStreamSerialization; attribute[0].val.programmaticStreamSerializationAllowed = 1; +cudaLaunchConfig_t configSecondary = {}; +configSecondary.gridDim = grid_dim; +configSecondary.blockDim = block_dim; +configSecondary.stream = stream; configSecondary.attrs = attribute; configSecondary.numAttrs = 1; @@ -187,8 +191,8 @@ edgeData.from_port = cudaGraphKernelNodePortProgrammatic; | Stream代码(简化) | 对应的图边设置 | |---|---| | `cudaLaunchAttributeProgrammaticStreamSerialization` 设为 1 | `edgeData.type = cudaGraphDependencyTypeProgrammatic`
`edgeData.from_port = cudaGraphKernelNodePortProgrammatic` | -| `cudaLaunchAttributeProgrammaticEvent` 且 `triggerAtBlockStart = 0` | 同上 | -| `cudaLaunchAttributeProgrammaticEvent` 且 `triggerAtBlockStart = 1` | `edgeData.type = cudaGraphDependencyTypeProgrammatic`
`edgeData.from_port = cudaGraphKernelNodePortLaunchCompletion` | +| `cudaLaunchAttributeProgrammaticEvent` 且 `triggerAtBlockStart = 0` | `edgeData.type = cudaGraphDependencyTypeProgrammatic`
` edgeData.from_port = cudaGraphKernelNodePortLaunchCompletion `| +| `cudaLaunchAttributeProgrammaticEvent` 且 `triggerAtBlockStart = 1` | `edgeData.type = cudaGraphDependencyTypeProgrammatic`
`edgeData.from_port = cudaGraphKernelNodePortLaunchStart` | ### 15.3.4 PDL的典型应用场景 @@ -235,8 +239,8 @@ cudaGraphInstantiate(&deviceGraphExec, deviceGraph, ```cuda // 方式1:实例化后显式上传 -cudaGraphInstantiate(&deviceGraphExec1, deviceGraph1, - cudaGraphInstantiateFlagDeviceLaunch); +cudaGraphInstantiateWithFlags(&deviceGraphExec1, deviceGraph1, + cudaGraphInstantiateFlagDeviceLaunch); cudaGraphUpload(deviceGraphExec1, stream); // 方式2:作为实例化的一部分上传 @@ -248,8 +252,8 @@ cudaGraphInstantiateWithParams(&deviceGraphExec2, deviceGraph2, &instantiateParams); // 方式3:通过主机端首次启动隐式上传 -cudaGraphInstantiate(&deviceGraphExec3, deviceGraph3, - cudaGraphInstantiateFlagDeviceLaunch); +cudaGraphInstantiateWithFlags(&deviceGraphExec3, deviceGraph3, + cudaGraphInstantiateFlagDeviceLaunch); cudaGraphLaunch(deviceGraphExec3, stream); // 隐式上传 ``` @@ -287,15 +291,14 @@ void graphSetup() { // 创建、实例化并上传设备图 create_graph(&g2); - cudaGraphInstantiate(&gExec2, g2, cudaGraphInstantiateFlagDeviceLaunch); + cudaGraphInstantiateWithFlags(&gExec2, g2, cudaGraphInstantiateFlagDeviceLaunch); cudaGraphUpload(gExec2, stream); // 创建并实例化启动图 cudaStreamBeginCapture(stream, cudaStreamCaptureModeGlobal); launchFireAndForgetGraph<<<1, 1, 0, stream>>>(gExec2); cudaStreamEndCapture(stream, &g1); - cudaGraphInstantiate(&gExec1, g1); - + cudaGraphInstantiateWithFlags(&gExec1, g1, 0); // 启动主机图,它将在内部启动设备图 cudaGraphLaunch(gExec1, stream); } @@ -450,7 +453,7 @@ void graphSetup() { cudaGraphCreate(&graph, 0); cudaGraphConditionalHandle handle; - cudaGraphConditionalHandleCreate(&handle, graph); + cudaGraphConditionalHandleCreate(&handle, graph,0,0); // 使用上游kernel设置条件值 cudaGraphNodeParams params = { cudaGraphNodeTypeKernel }; @@ -476,7 +479,7 @@ void graphSetup() { // ... cudaGraphAddNode(&node, elseBodyGraph, NULL, NULL, 0, ¶ms); - cudaGraphInstantiate(&graphExec, graph, NULL, NULL, 0); + cudaGraphInstantiateWithFlags(&graphExec, graph, 0); cudaGraphLaunch(graphExec, 0); cudaDeviceSynchronize(); @@ -492,10 +495,14 @@ WHILE节点的body图会在条件为非零时持续执行。条

图 15.13 条件WHILE节点

```cuda +__device__ int loopCount = 10; + __global__ void loopKernel(cudaGraphConditionalHandle handle) { - static int count = 10; - cudaGraphSetConditional(handle, --count ? 1 : 0); + if (threadIdx.x == 0 && blockIdx.x == 0) { + int val = atomicSub(&loopCount, 1); + cudaGraphSetConditional(handle, val > 1 ? 1 : 0); + } } void graphSetup() { @@ -504,7 +511,7 @@ void graphSetup() { cudaGraphNode_t node; void *kernelArgs[1]; - cuGraphCreate(&graph, 0); + cudaGraphCreate(&graph, 0); cudaGraphConditionalHandle handle; // 使用默认值1,避免需要上游kernel来设置初始条件 @@ -527,7 +534,7 @@ void graphSetup() { kernelArgs[0] = &handle; cudaGraphAddNode(&node, bodyGraph, NULL, NULL, 0, ¶ms); - cudaGraphInstantiate(&graphExec, graph, NULL, NULL, 0); + cudaGraphInstantiateWithFlags(&graphExec, graph, 0); cudaGraphLaunch(graphExec, 0); cudaDeviceSynchronize(); @@ -563,7 +570,7 @@ void graphSetup() { // 填充最后一个分支 cudaGraphAddNode(&node, bodyGraphs[4], NULL, NULL, 0, ¶ms); - cudaGraphInstantiate(&graphExec, graph, NULL, NULL, 0); + cudaGraphInstantiateWithFlags(&graphExec, graph, 0); cudaGraphLaunch(graphExec, 0); cudaDeviceSynchronize(); } @@ -621,7 +628,9 @@ params.bytesize = size; cudaGraphAddMemAllocNode(&allocNode, graph, NULL, 0, ¶ms); // 在分配之后使用内存的kernel节点 -nodeParams->kernelParams[0] = params.dptr; +void* dptr = (void*)(uintptr_t)params.dptr; +void* kernelArgs[] = { &dptr }; +nodeParams->kernelParams = kernelArgs; cudaGraphAddKernelNode(&a, graph, &allocNode, 1, &nodeParams); cudaGraphAddKernelNode(&b, graph, &a, 1, &nodeParams); cudaGraphAddKernelNode(&c, graph, &a, 1, &nodeParams); @@ -668,7 +677,7 @@ cudaGraph_t originalGraph, updatedGraph; // 创建并实例化原始图 createGraph(&originalGraph); -cudaGraphInstantiate(&graphExec, originalGraph); +cudaGraphInstantiateWithFlags(&graphExec, originalGraph, 0); // 多次启动... for (int i = 0; i < numIterations; i++) { diff --git "a/outputs/gpu-programming-course/docs/advanced-chapter16/\347\254\25416\347\253\240 Cooperative Groups\346\211\251\345\261\225.md" "b/outputs/gpu-programming-course/docs/advanced-chapter16/\347\254\25416\347\253\240 Cooperative Groups\346\211\251\345\261\225.md" index 07efe3b..629fc83 100644 --- "a/outputs/gpu-programming-course/docs/advanced-chapter16/\347\254\25416\347\253\240 Cooperative Groups\346\211\251\345\261\225.md" +++ "b/outputs/gpu-programming-course/docs/advanced-chapter16/\347\254\25416\347\253\240 Cooperative Groups\346\211\251\345\261\225.md" @@ -81,12 +81,13 @@ CG自CUDA 11.5以来经历了显著扩展。以下是各版本的关键新增功 CG的组类型形成了一个层次结构: ``` -coalesced_group (warp中活跃线程) - └── thread_block_tile (编译期大小的tile) - └── 派生自 thread_block -thread_block (线程块中所有线程) - └── cluster_group (集群中所有线程/块) [CC 9.0+] - └── grid_group (网格中所有线程) [需要cooperative launch] + +thread_group(抽象基类) + ├── coalesced_group(warp中活跃线程) + ├── thread_block_tile(编译期大小的tile) + ├── thread_block(线程块中所有线程) + ├── cluster_group(集群中所有线程/块)[CC 9.0+] + └── grid_group(网格中所有线程)[需要cooperative launch] ``` --- @@ -123,14 +124,13 @@ __global__ void clusterKernel() { | 成员函数 | 返回类型 | 说明 | |---------|---------|------| -| `sync()` | `static void` | 集群级别同步,等价于 `barrier_wait(barrier_arrive())` | -| `barrier_arrive()` | `cluster_group::arrival_token` | 到达集群屏障,返回token | -| `barrier_wait(token&&)` | `static void` | 等待集群屏障,接收arrive返回的token | +| `sync()` | `void` | 集群级别同步,等价于 `barrier_wait(barrier_arrive())` | +| `barrier_arrive()` | `arrival_token` | 到达集群屏障,返回token | +| `barrier_wait(token&&)` | `void` | 等待集群屏障,接收arrive返回的token | | `thread_rank()` | `static unsigned int` | 调用线程在集群中的排名 [0, num_threads) | | `block_rank()` | `static unsigned int` | 调用线程所在块在集群中的排名 [0, num_blocks) | | `num_threads()` | `static unsigned int` | 集群中的总线程数 | | `num_blocks()` | `static unsigned int` | 集群中的总线程块数 | -| `dim_threads()` | `static dim3` | 集群的线程维度 | | `dim_blocks()` | `static dim3` | 集群的线程块维度 | | `block_index()` | `static dim3` | 调用块在集群中的3D索引 | | `query_shared_rank(const void *addr)` | `static unsigned int` | 查询共享内存地址属于哪个块 | @@ -162,14 +162,14 @@ __global__ void dsmKernel() { ```cuda __global__ void dsmAccessKernel() { - __shared__ float localCounter; + __shared__ unsigned int localCounter; cg::cluster_group cluster = cg::this_cluster(); unsigned int myBlockRank = cluster.block_rank(); if (myBlockRank == 0) { // 块0:初始化计数器 - localCounter = 0.0f; + localCounter = 0; } cluster.sync(); @@ -180,12 +180,12 @@ __global__ void dsmAccessKernel() { ); // 跨块原子加操作 - atomicAdd(remoteCounter, 1.0f); + atomicAdd(remoteCounter, 1); cluster.sync(); if (myBlockRank == 0) { - printf("Total increments: %f (should be %u)\n", - localCounter, cluster.num_blocks()); + printf("Total increments: %u (should be %u)\n", + localCounter, cluster.num_blocks()); } } ``` @@ -335,13 +335,10 @@ __global__ void asyncReduceKernel(float *input, float *output, int n) { cg::plus()); // 在等待reduce完成的同时,执行其他独立计算 - float otherWork = doSomeIndependentCalculation(threadIdx.x); - - // 等待异步操作完成 + float localResult = doSomeIndependentCalculation(threadIdx.x); barrier.arrive_and_wait(); - if (block.thread_rank() == 0) { - *output = s_data[0] + otherWork; + *output = s_data[0]; } } ``` @@ -481,6 +478,7 @@ config.blockDim = blockDim; config.dynamicSmemBytes = sharedMemSize; // 必须使用协同启动API +void *args[] = {&data, &n}; cudaLaunchCooperativeKernel((void*)gridSyncKernel, config.gridDim, config.blockDim, args, config.dynamicSmemBytes, stream); @@ -611,10 +609,10 @@ __global__ void distributedHistogram( // 获取集群组 cg::cluster_group cluster = cg::this_cluster(); unsigned int numBlocks = cluster.num_blocks(); + unsigned int myBlockRank = cluster.block_rank(); - // 每个块分配一部分共享内存用于直方图 + // 每个块分配相同大小的共享内存用于直方图 __shared__ unsigned int localBins[NUM_BINS]; - extern __shared__ unsigned int sharedBins[]; // 初始化本地共享内存 for (int i = threadIdx.x; i < NUM_BINS; i += blockDim.x) { @@ -624,10 +622,9 @@ __global__ void distributedHistogram( // 分布式全局索引分配 // 每个块处理一部分数据 - unsigned int itemsPerBlock = N / numBlocks; - unsigned int blockStart = cluster.block_rank() * itemsPerBlock; - unsigned int blockEnd = (cluster.block_rank() == numBlocks - 1) ? - N : blockStart + itemsPerBlock; + unsigned int itemsPerBlock = (N + numBlocks - 1) / numBlocks; // 向上取整,避免数据丢失 + unsigned int blockStart = myBlockRank * itemsPerBlock; + unsigned int blockEnd = min(blockStart + itemsPerBlock, (unsigned int)N); // 阶段1:本地直方图计算 for (int i = blockStart + threadIdx.x; i < blockEnd; i += blockDim.x) { @@ -636,11 +633,20 @@ __global__ void distributedHistogram( } cluster.sync(); + // 预先获取所有块的共享内存地址(避免循环中重复调用map_shared_rank) + __shared__ unsigned int *remoteBins[32]; // 集群最大32个块 + if (threadIdx.x == 0) { + for (unsigned int b = 0; b < numBlocks; b++) { + remoteBins[b] = cluster.map_shared_rank(localBins, b); + } + } + cluster.sync(); + // 阶段2:使用分布式共享内存进行全局合并 // 每个块负责合并一定数量的bin - unsigned int binsPerBlock = NUM_BINS / numBlocks; - unsigned int myBinStart = cluster.block_rank() * binsPerBlock; - unsigned int myBinEnd = myBinStart + binsPerBlock; + unsigned int binsPerBlock = (NUM_BINS + numBlocks - 1) / numBlocks; // 向上取整 + unsigned int myBinStart = myBlockRank * binsPerBlock; + unsigned int myBinEnd = min(myBinStart + binsPerBlock, (unsigned int)NUM_BINS); for (unsigned int bin = myBinStart + threadIdx.x; bin < myBinEnd; bin += blockDim.x) { @@ -648,15 +654,11 @@ __global__ void distributedHistogram( // 从集群中所有块收集该bin的计数 for (unsigned int b = 0; b < numBlocks; b++) { - unsigned int *remoteBins = - cluster.map_shared_rank(localBins, b); - total += remoteBins[bin]; + total += remoteBins[b][bin]; } // 写入全局内存 - if (threadIdx.x < binsPerBlock) { - globalHistogram[bin] = total; - } + globalHistogram[bin] = total; } } ``` diff --git "a/outputs/gpu-programming-course/docs/advanced-chapter17/\347\254\25417\347\253\240 \347\216\260\344\273\243GPU\346\236\266\346\236\204\346\246\202\350\247\210\344\270\216C++20\346\224\257\346\214\201.md" "b/outputs/gpu-programming-course/docs/advanced-chapter17/\347\254\25417\347\253\240 \347\216\260\344\273\243GPU\346\236\266\346\236\204\346\246\202\350\247\210\344\270\216C++20\346\224\257\346\214\201.md" index 9103e3f..3c43c93 100644 --- "a/outputs/gpu-programming-course/docs/advanced-chapter17/\347\254\25417\347\253\240 \347\216\260\344\273\243GPU\346\236\266\346\236\204\346\246\202\350\247\210\344\270\216C++20\346\224\257\346\214\201.md" +++ "b/outputs/gpu-programming-course/docs/advanced-chapter17/\347\254\25417\347\253\240 \347\216\260\344\273\243GPU\346\236\266\346\236\204\346\246\202\350\247\210\344\270\216C++20\346\224\257\346\214\201.md" @@ -310,7 +310,10 @@ struct Vec3 { __global__ void compareVectors(Vec3 *a, Vec3 *b, int *result) { // 使用三路比较运算符 - *result = (*a < *b) ? -1 : ((*b < *a) ? 1 : 0); + auto cmp = *a <=> *b; + if (cmp < 0) *result = -1; + else if (cmp > 0) *result = 1; + else *result = 0; } ``` diff --git "a/outputs/gpu-programming-course/docs/advanced-chapter2/\347\254\25413\347\253\240 \345\274\202\346\255\245SIMT\347\274\226\347\250\213\346\250\241\345\236\213.md" "b/outputs/gpu-programming-course/docs/advanced-chapter2/\347\254\25413\347\253\240 \345\274\202\346\255\245SIMT\347\274\226\347\250\213\346\250\241\345\236\213.md" index cfd944d..171a389 100644 --- "a/outputs/gpu-programming-course/docs/advanced-chapter2/\347\254\25413\347\253\240 \345\274\202\346\255\245SIMT\347\274\226\347\250\213\346\250\241\345\236\213.md" +++ "b/outputs/gpu-programming-course/docs/advanced-chapter2/\347\254\25413\347\253\240 \345\274\202\346\255\245SIMT\347\274\226\347\250\213\346\250\241\345\236\213.md" @@ -434,10 +434,10 @@ __global__ void single_stage_kernel(int *data, int *result, size_t size) { // 消费者:等待数据就绪 pipeline.consumer_wait(); - // 计算 - for (int i = threadIdx.x; i < block.size(); i += block.size()) { - result[offset + i] = shared[i] * 2; - } + + // 计算:每个线程处理自己对应的元素 + int i = threadIdx.x; + result[offset + i] = shared[i] * 2; // 释放阶段 pipeline.consumer_release(); @@ -479,24 +479,30 @@ __global__ void pipeline_kernel(int *data, int *result, size_t size) { pipeline.producer_commit(); } - // 主循环:消费和生产的流水线重叠 + // 主循环:消费和生产的流水线重叠 + // 注意:生产者写入的缓冲区索引必须不同于消费者读取的索引 for (size_t i = 0; i < num_blocks - (stages - 1); i++) { - int stage = i % stages; + int producer_stage = (i + stages - 1) % stages; // 生产者写入的缓冲区 + int consumer_stage = i % stages; // 消费者读取的缓冲区 // 生产下一批数据(异步拷贝) pipeline.producer_acquire(); size_t producer_offset = (i + stages - 1) * block.size(); if (producer_offset < size) { - cuda::memcpy_async(block, buffer[stage], + cuda::memcpy_async(block, buffer[producer_stage], data + producer_offset, sizeof(int) * block.size(), pipeline); } pipeline.producer_commit(); + // 等待生产者提交完成(确保所有线程的提交操作就绪) + __syncthreads(); + // 消费当前阶段的数据 pipeline.consumer_wait(); - for (int j = threadIdx.x; j < block.size(); j += block.size()) { - result[i * block.size() + j] = buffer[stage][j] * 2; + int j = threadIdx.x; + if (j < block.size()) { + result[i * block.size() + j] = buffer[consumer_stage][j] * 2; } pipeline.consumer_release(); } @@ -596,18 +602,29 @@ if (any_thread_needs_copy) { ### 13.7.5 完成函数(Completion Function) -CUDA Programming Guide 10.26.7 节引入了 Completion Function 的概念。这是一个在 barrier 翻转时自动调用的函数: +cuda::barrier 支持一个可选的完成函数,它在屏障每次相位翻转时自动执行。完成函数在最后一个 `arrive() `调用触发翻转时运行,且在所有被阻塞的 `wait() `唤醒之前执行。 + +完成函数通过模板参数指定,而不是运行时设置: ```cuda -// 概念:在 barrier 完成时自动执行的清理或通知操作 -bar.init(block.size()); -bar.set_completion_function([]() { - // 当 barrier 翻转时自动执行 - // 例如:递增共享计数器,触发下一个阶段处理等 -}); +// 定义一个完成函数(可以是一个函数对象) +struct MyCompletion { + __device__ void operator()() { + // 当屏障翻转时自动执行,例如:递增共享计数器 + } +}; + +// 使用完成函数作为模板参数 +using barrier_with_completion = cuda::barrier< + cuda::thread_scope::thread_scope_block, + MyCompletion +>; + +__shared__ barrier_with_completion bar; +// ... init(&bar, block.size()); ``` -完成函数在 barrier 的最后一个 `arrive()` 调用触发翻转时执行,在所有被阻塞的线程被唤醒之前执行。这提供了在同步点自动执行操作的能力。 +完成函数是 CUDA 12.0 及以上版本引入的特性,低版本不支持;完成函数由最后一个到达的线程执行;完成函数中不能调用任何可能阻塞的操作(如` wait()`、`memcpy_async` 等),否则会导致死锁。 ## 13.8 性能测量与分析 @@ -899,7 +916,7 @@ int main() { // 启动 pipeline 核函数 size_t shared_mem = stages * threads_per_block * sizeof(float); - pipeline_demo_kernel<<<1, threads_per_block, shared_mem>>>( + pipeline_demo_kernel<<<1, threads_per_block, shared_mem>>>( d_input, d_output, N, scale); CUDA_CHECK(cudaDeviceSynchronize()); @@ -939,7 +956,7 @@ int main() { 在稳态下,Load 和 Compute 完全重叠。阶段数越多,越容易在拷贝延迟波动时维持重叠,但也会消耗更多共享内存。 -## 13.8 同步机制的对比 +## 13.11 同步机制的对比 | 特性 | `__syncthreads()` | `cuda::barrier` | `cuda::pipeline` | |------|------------------|-----------------|-----------------| @@ -957,9 +974,9 @@ CUDA Programming Guide 也给出了建议: 即:对于不需要异步重叠的简单同步场景,使用传统的 `__syncthreads()` 仍然是最优选择。 -## 13.9 异步操作的调试与错误处理 +## 13.12 异步操作的调试与错误处理 -### 13.9.1 常见问题诊断 +### 13.12.1 常见问题诊断 使用异步操作时,常见的问题包括: @@ -990,15 +1007,15 @@ bar.wait(std::move(token)); // 使用 phase N 的过期 token 4. Pipeline 阶段溢出:`producer_acquire()` 超过 pipeline 的阶段数导致死锁。 -### 13.9.2 使用环境变量和工具调试 +### 13.12.2 使用环境变量和工具调试 - `CUDA_LAUNCH_BLOCKING=1`:使所有 kernel 启动变为同步,便于隔离问题; - `cuda-memcheck` 工具检测共享内存的非法访问; - NVIDIA Compute Sanitizer 可以检测异步操作中的数据竞争。 -## 13.10 高级 Pipeline 模式 +## 13.13 高级 Pipeline 模式 -### 13.10.1 Pipeline 与 GEMM 的软件流水线 +### 13.13.1 Pipeline 与 GEMM 的软件流水线 在矩阵乘法(GEMM)中,多阶段 Pipeline 用于实现 Global-to-Shared 内存拷贝与 Tensor Core 计算的重叠: @@ -1066,7 +1083,7 @@ __global__ void gemm_pipeline( } ``` -### 13.10.2 动态阶段数选择 +### 13.13.2 动态阶段数选择 Pipeline 的阶段数涉及权衡: @@ -1084,7 +1101,7 @@ int select_pipeline_stages(size_t tile_bytes, size_t smem_per_block) { } ``` -### 13.10.3 Pipeline 交错模式 +### 13.13.3 Pipeline 交错模式 ```cuda // 双 Pipeline 交错 @@ -1101,7 +1118,7 @@ pa.consumer_wait(); compute_a(); pa.consumer_release(); pb.consumer_wait(); compute_b(); pb.consumer_release(); ``` -## 13.11 与主机端异步操作的对比 +## 13.14 与主机端异步操作的对比 | 特性 | 主机端异步 (cudaMemcpyAsync) | 设备端异步 (memcpy_async) | |------|---------------------------|-------------------------| @@ -1114,9 +1131,9 @@ pb.consumer_wait(); compute_b(); pb.consumer_release(); 两种异步方式可以同时使用:设备端 pipeline 负责细粒度的 Global→Shared 重叠,主机端 Stream 负责不同 kernel 间的粗粒度重叠。 -## 13.13 常见问题与故障排除 +## 13.15 常见问题与故障排除 -### 13.13.1 `cuda::barrier` 死锁 +### 13.15.1 `cuda::barrier` 死锁 症状:所有线程卡在 `bar.wait()` 调用上。 @@ -1130,7 +1147,7 @@ pb.consumer_wait(); compute_b(); pb.consumer_release(); - 确保所有参与线程都在同一个代码路径中调用 `arrive()`; - 每次迭代使用新的 token。 -### 13.13.2 Pipeline 死锁 +### 13.15.2 Pipeline 死锁 症状:`producer_acquire()` 永不返回。 @@ -1146,7 +1163,7 @@ for (int i = 0; i < num_iter; i++) { // 几个迭代后死锁 ``` -### 13.13.3 `memcpy_async` 性能差 +### 13.15.3 `memcpy_async` 性能差 常见原因: 1. 拷贝大小不是 16 字节的倍数(回退到逐字节拷贝); @@ -1160,7 +1177,7 @@ for (int i = 0; i < num_iter; i++) { - 确保全局内存基地址 128 字节对齐; - 在 commit/wait 前使用 `__syncwarp()` 恢复 warp 收敛。 -### 13.13.4 数据完整性错误 +### 13.15.4 数据完整性错误 症状:某些输出值不正确或为零。 @@ -1170,7 +1187,7 @@ for (int i = 0; i < num_iter; i++) { 3. 确认 `producer_commit()` 在所有异步拷贝之后调用; 4. 检查 `fence_proxy_async_shared_cta()` 是否在写回前调用(如果使用 TMA)。 -## 13.14 性能基准参考 +## 13.16 性能基准参考 以下是在 A100 (CC 8.0) 上进行批量数据处理(100 批次,256 个 float 每批次)的性能参考: @@ -1189,7 +1206,7 @@ for (int i = 0; i < num_iter; i++) { 2. 多阶段 pipeline 带来显著提升(2-stage 比单阶段快 36%); 3. 从 3-stage 到 4-stage 的增量很小,因为瓶颈从拷贝转向了计算。 -## 13.15 迁移指南:从 __syncthreads 到异步模型 +## 13.17 迁移指南:从 __syncthreads 到异步模型 如果你的现有代码使用传统的 `__syncthreads()` 模式,迁移到异步模型应该循序渐进: @@ -1248,7 +1265,7 @@ for (int i = 0; i < n; i++) { - 验证计算和数据拷贝是否有实际重叠; - 调整 pipeline 阶段数以平衡共享内存和吞吐量。 -## 13.16 动手体验2:异步归约 +## 13.18 动手体验2:异步归约 在第 12 章我们使用 Thread Block Cluster + DSM 进行分布式归约。这里展示一个使用 `cuda::barrier` 进行分阶段异步归约的替代方案: @@ -1330,7 +1347,7 @@ for (int i = 0; i < n; i++) { } ``` -## 13.17 本章小结 +## 13.19 本章小结 本章全面介绍了 CUDA 异步 SIMT 编程模型,涵盖以下要点: @@ -1348,7 +1365,7 @@ for (int i = 0; i < n; i++) { 异步 SIMT 编程模型是现代 GPU 编程中不可或缺的工具。随着 GPU 内存带宽与计算能力之间的差距不断拉大,将数据搬运隐藏在计算之后变得愈发重要。掌握这些 API 将显著提升你的 CUDA 程序性能。 -## 13.10 习题 +## 13.20 习题 1. 解释 `cuda::barrier` 的"时间分割"(Temporal Splitting)五阶段模型。为什么 arrive 和 wait 之间的阶段是实现重叠的关键? @@ -1362,7 +1379,7 @@ for (int i = 0; i < n; i++) { 6. 说明 Warp Specialization 模式中为什么需要 4 个 barrier(2×2双缓冲),而不是 2 个。 -## 13.11 参考文献 +## 13.21 参考文献 1. CUDA C++ Programming Guide 13.0, Section 5.5 "Asynchronous SIMT Programming Model" 2. CUDA C++ Programming Guide 13.0, Section 10.26 "Asynchronous Barrier" diff --git "a/outputs/gpu-programming-course/docs/advanced-chapter3/\347\254\25414\347\253\240 Tensor Memory Accelerator.md" "b/outputs/gpu-programming-course/docs/advanced-chapter3/\347\254\25414\347\253\240 Tensor Memory Accelerator.md" index f184d55..b7fe50e 100644 --- "a/outputs/gpu-programming-course/docs/advanced-chapter3/\347\254\25414\347\253\240 Tensor Memory Accelerator.md" +++ "b/outputs/gpu-programming-course/docs/advanced-chapter3/\347\254\25414\347\253\240 Tensor Memory Accelerator.md" @@ -80,7 +80,7 @@ CUDA Programming Guide 原表(Table 8): | Swizzle | 不支持 | 硬件支持(4种模式) | | 多播 | 不支持 | 支持 Cluster 多播 | | 拷贝大小 | 4/8/16 字节对齐 | 16 字节对齐 | -| 共享内存对齐 | 128 字节 | 128 字节(多维) | +| 共享内存对齐 | 16 字节(最大) | 128 字节(多维) | ## 14.3 一维 TMA 拷贝 @@ -239,6 +239,14 @@ CUresult res = cuTensorMapEncodeTiled( CUtensorMapL2promotion::CU_TENSOR_MAP_L2_PROMOTION_NONE, CUtensorMapFloatOOBfill::CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE ); + +//检查返回值 +if (res != CUDA_SUCCESS) { + const char* errStr = "unknown"; + cuGetErrorString(res, &errStr); + fprintf(stderr, "cuTensorMapEncodeTiled failed with error %d: %s\n", res, errStr); + exit(EXIT_FAILURE); +} ``` `cuTensorMapEncodeTiled` 的参数含义: @@ -365,10 +373,6 @@ __global__ void kernel(const __grid_constant__ CUtensorMap tensor_map, cde::cp_async_bulk_wait_group_read<0>(); } - // 销毁 barrier,释放共享内存 - if (threadIdx.x == 0) { - (&bar)->~barrier(); - } } ``` @@ -846,7 +850,7 @@ __global__ void tma_pipeline_kernel( } ``` -## 14.9 TMA vs 手动 memcpy_async 性能对比 +## 14.12 TMA vs 手动 memcpy_async 性能对比 | 维度 | 手动 memcpy_async | TMA | |------|-----------------|-----| @@ -861,29 +865,29 @@ __global__ void tma_pipeline_kernel( 对于简单的 1D 连续数据拷贝,TMA 和 `memcpy_async` 性能接近。但当涉及多维分块、不规则步长、或需要消除 bank conflict 的场景时,TMA 的优势就非常显著——因为它将复杂的地址计算和布局转换卸载到了专用硬件。 -## 14.10 TMA 的限制与兼容性 +## 14.13 TMA 的限制与兼容性 -### 14.10.1 硬件限制 +### 14.13.1 硬件限制 1. CC 9.0+ 独占:TMA 是 Hopper 架构的特性,无法在旧硬件上使用; 2. 共享内存对齐严格:多维 TMA 要求共享内存 128 字节对齐,Swizzle 模式下要求 1024 字节对齐; 3. 异步性不保证:CUDA Programming Guide 明确指出 TMA 传输的异步性取决于硬件实现; 4. 多播性能:`sm_90a` 目标上优化最佳,其他目标可能性能显著下降。 -### 14.10.2 编程限制 +### 14.13.2 编程限制 1. Tensor Map 的创建需要 Driver API(`cuTensorMapEncodeTiled`),不能完全在 Runtime API 中完成; 2. 设备端编码依赖 `tensormap.replace` PTX 指令,API 封装尚在 experimental 阶段; 3. 调试困难:TMA 是硬件黑盒,无法像手动 `memcpy_async` 那样直接跟踪数据流; 4. 编译器要求:需要 `nvcc -arch=sm_90` 或更高。 -### 14.10.3 兼容性 +### 14.13.3 兼容性 - TMA 代码在 CC < 9.0 的设备上无法运行; - 可以在编译时检查: ```cuda -#if defined(__CUDA_MINIMUM_ARCH__) && __CUDA_MINIMUM_ARCH__ < 900 +#if __CUDA_ARCH__ < 900 static_assert(false, "Device code compiled with older architectures incompatible with TMA."); #endif @@ -900,15 +904,15 @@ static_assert(false, for (int row = 0; row < SMEM_HEIGHT; row++) { cuda::memcpy_async(block, &smem[row * SMEM_WIDTH], - &global[global_y + row) * GMEM_WIDTH + global_x], + &global[(global_y + row) * GMEM_WIDTH + global_x], sizeof(float) * SMEM_WIDTH, pipe); } #endif ``` -## 14.11 TMA 实际应用场景 +## 14.14 TMA 实际应用场景 -### 14.11.1 深度学习卷积 +### 14.14.1 深度学习卷积 在深度学习框架中,卷积操作的 im2col 或直接卷积实现是 TMA 的典型应用: @@ -936,7 +940,7 @@ __global__ void conv3d_tma( } ``` -### 14.11.2 矩阵乘法分块 +### 14.14.2 矩阵乘法分块 ```cuda __global__ void gemm_tma( @@ -982,7 +986,7 @@ __global__ void gemm_tma( } ``` -### 14.11.3 图像处理中的边界处理 +### 14.14.3 图像处理中的边界处理 利用 TMA 的越界零填充机制,可以简化图像处理中的边界处理: @@ -1003,9 +1007,9 @@ __global__ void convolution_with_padding( } ``` -## 14.12 TMA 性能调优指南 +## 14.15 TMA 性能调优指南 -### 14.12.1 拷贝大小的选择 +### 14.15.1 拷贝大小的选择 TMA 的最佳拷贝大小取决于多个因素: @@ -1019,7 +1023,7 @@ TMA 的最佳拷贝大小取决于多个因素: > "In general, issuing as few bulk copies with as big a size as possible results in the best performance." -### 14.12.2 对齐的严格性 +### 14.15.2 对齐的严格性 下表总结了不同 TMA 模式的对齐要求严重程度: @@ -1030,7 +1034,7 @@ TMA 的最佳拷贝大小取决于多个因素: | 步长 16B 倍数 | 硬错误(未定义行为) | | Swizzle 共享内存 1024B 对齐 | 硬错误(128B Swizzle 模式) | -### 14.12.3 使用 Nsight Compute 分析 TMA 性能 +### 14.15.3 使用 Nsight Compute 分析 TMA 性能 NVIDIA Nsight Compute 提供 TMA 相关指标: @@ -1040,7 +1044,7 @@ NVIDIA Nsight Compute 提供 TMA 相关指标: 通过这些指标可以量化 TMA 实际节省的指令数和带宽。 -## 14.13 TMA 与手动 memcpy_async 的迁移指南 +## 14.16 TMA 与手动 memcpy_async 的迁移指南 如果你的代码目前使用 `cuda::memcpy_async` 进行多维分块拷贝,迁移到 TMA 的步骤如下: @@ -1058,7 +1062,7 @@ NVIDIA Nsight Compute 提供 TMA 相关指标: -## 14.11 动手体验:完整的 2D TMA 分块处理程序 +## 14.17 动手体验:完整的 2D TMA 分块处理程序 下面是一个完整的、概念展示性的 2D TMA 程序(注意:实际运行需要 H100 硬件和 Driver API 支持): @@ -1121,6 +1125,14 @@ CUtensorMap create_2d_tensor_map(int *d_data) { CUtensorMapSwizzle::CU_TENSOR_MAP_SWIZZLE_NONE, CUtensorMapL2promotion::CU_TENSOR_MAP_L2_PROMOTION_NONE, CUtensorMapFloatOOBfill::CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE); + + if (res != CUDA_SUCCESS) { + const char* errStr = "unknown"; + cuGetErrorString(res, &errStr); + fprintf(stderr, "cuTensorMapEncodeTiled failed with error %d: %s\n", res, errStr); + exit(EXIT_FAILURE); + } + return tmap; } @@ -1150,9 +1162,9 @@ int main() { } ``` -## 14.14 常见问题与故障排除 +## 14.18 常见问题与故障排除 -### 14.14.1 TMA 传输返回错误或崩溃 +### 14.18.1 TMA 传输返回错误或崩溃 常见原因: 1. 全局内存地址未 16 字节对齐; @@ -1165,7 +1177,7 @@ int main() { - 共享内存使用 `__shared__ alignas(128)` 或 `alignas(1024)`(Swizzle); - 使用 `static_assert` 在编译时检查计算能力。 -### 14.14.2 Tensor Map 创建失败 +### 14.18.2 Tensor Map 创建失败 cuTensorMapEncodeTiled 返回错误: @@ -1174,7 +1186,7 @@ int main() { 2. `stride` 不是 16 字节的倍数; 3. Driver API 版本不匹配(需要 CUDA 12.0+ 驱动)。 -### 14.14.3 Swizzle 索引计算错误 +### 14.18.3 Swizzle 索引计算错误 症状:使用 Swizzle 后数据正确但位置错误。 @@ -1183,7 +1195,7 @@ int main() { 2. 正确计算偏移量:`offset = (reinterpret_cast(smem_ptr)/128)%8`; 3. 正确使用索引关系:`smem[y][((y+offset)%8)^x]`。 -### 14.14.4 设备端编码的 fence 问题 +### 14.18.4 设备端编码的 fence 问题 症状:修改后的 Tensor Map 不可见或使用了旧值。 @@ -1192,7 +1204,7 @@ int main() { - 消费 kernel 中使用 `fence_proxy_tensormap_generic` 的 acquire fence; - 确保编码和消费在不同的 kernel launch 中(或在同一 launch 中使用适当的内存顺序)。 -### 14.14.5 TMA 多播性能不佳 +### 14.18.5 TMA 多播性能不佳 可能原因: 1. 未使用 `sm_90a` 目标编译; @@ -1204,7 +1216,7 @@ int main() { - 将多播限制在较小的 cluster(2-4 个块); - 评估是否真的需要多播——有时单块加载 + DSM 分发更高效。 -## 14.15 性能基准参考 +## 14.19 性能基准参考 以下是在 H100 (CC 9.0) 上对不同数据搬运方案的性能对比(2D 256x256 数组分块,tile 16x16): @@ -1222,7 +1234,7 @@ int main() { 2. 二维 TMA tensor copy 比一维快约 33%,因为一次指令处理了整个 tile; 3. Swizzle 模式在转置场景提供了额外 33% 的加速,来自消除 bank conflict。 -## 14.16 迁移指南:从 memcpy_async 到 TMA +## 14.20 迁移指南:从 memcpy_async 到 TMA 如果你的代码目前使用 `cuda::memcpy_async` 进行多维分块拷贝,以下是迁移到 TMA 的步骤: @@ -1281,7 +1293,7 @@ __syncthreads(); #endif ``` -## 14.17 本章小结 +## 14.21 本章小结 本章深入介绍了 NVIDIA Hopper 架构的 Tensor Memory Accelerator (TMA),要点总结如下: @@ -1303,7 +1315,7 @@ __syncthreads(); TMA 代表了 GPU 编程从"软件管理数据搬运"到"硬件加速数据搬运"的重要演进。对于矩阵乘法、卷积、转置等核心计算模式,TMA 能够显著简化代码并提升性能。 -## 14.13 习题 +## 14.22 习题 1. 对比 TMA 的 bulk-asynchronous copy 和上一章的 `cuda::memcpy_async`,说明在什么场景下 TMA 有显著优势,什么场景下两者性能接近。 @@ -1317,7 +1329,7 @@ TMA 代表了 GPU 编程从"软件管理数据搬运"到"硬件加速数据搬 6. 讨论设备端 Tensor Map 编码相比主机端创建的优势和局限性。什么场景下必须使用设备端编码? -## 14.14 参考文献 +## 14.23 参考文献 1. CUDA C++ Programming Guide 13.0, Section 10.29 "Asynchronous Data Copies using the Tensor Memory Accelerator (TMA)" 2. CUDA C++ Programming Guide 13.0, Section 10.30 "Encoding a Tensor Map on Device" diff --git "a/outputs/gpu-programming-course/docs/chapter1/\347\254\2541\347\253\240 GPU\350\256\241\347\256\227\344\270\216CUDA\345\205\245\351\227\250.md" "b/outputs/gpu-programming-course/docs/chapter1/\347\254\2541\347\253\240 GPU\350\256\241\347\256\227\344\270\216CUDA\345\205\245\351\227\250.md" index ef67947..bce22a1 100644 --- "a/outputs/gpu-programming-course/docs/chapter1/\347\254\2541\347\253\240 GPU\350\256\241\347\256\227\344\270\216CUDA\345\205\245\351\227\250.md" +++ "b/outputs/gpu-programming-course/docs/chapter1/\347\254\2541\347\253\240 GPU\350\256\241\347\256\227\344\270\216CUDA\345\205\245\351\227\250.md" @@ -22,7 +22,7 @@ - 复杂的控制逻辑:分支预测(Branch Prediction)、乱序执行(Out-of-Order Execution)、超标量流水线(Superscalar Pipeline)等 - 大型缓存层次结构:L1、L2、L3缓存,以减少指令和数据访问的平均延迟 -这种设计使得CPU能够高效地执行那些具有复杂控制流、大量分支和不可预测内存访问模式的程序。一个典型的现代CPU核心可以同时执行几十个(通常是2-4个硬件线程)线程。 +这种设计使得CPU能够高效地执行那些具有复杂控制流、大量分支和不可预测内存访问模式的程序。一个典型的现代 CPU 整机可以同时执行几十到上百个线程,每个核心通常支持 2-4 个硬件线程 而图形处理单元(GPU)的设计目标截然不同。GPU被设计为能够在同一时刻执行数千个线程,以最大化整体吞吐量(Throughput)。为了达成这一目标,GPU将更多晶体管用于数据计算(如浮点运算单元ALU),而非数据缓存和流控制。 @@ -40,7 +40,7 @@ CUDA Programming Guide对这两种设计理念给出了权威的阐述: | 并发线程数 | 几十个(~2-64 per core) | 数千至数万个 | | 晶体管分配 | 大型缓存 + 复杂控制逻辑 | 大量ALU + 精简控制 | | 内存延迟处理 | 大缓存 + 预取 | 线程切换掩藏延迟 | -| 时钟频率 | 更高(~3-5 GHz) | 相对较低(~1-2 GHz) | +| 时钟频率 | 更高(~3-5 GHz) | 相对较低(~1-3 GHz) | | 单线程性能 | 极强 | 较弱 | | 总吞吐量 | 中等 | 极高 | @@ -69,7 +69,7 @@ GPU采用"晶体管换ALU"策略的一个核心动机,来源于它们在处理 CPU通过缓存来避免内存延迟:大容量的L1/L2/L3缓存层次结构使得CPU可以在大部分时间从高速缓存而非慢速主存获取数据,从而避免长时间的DRAM访问等待。 -GPU则通过计算来隐藏内存延迟:当一个线程发起内存请求并等待数据返回时(这可能需要数百个时钟周期),GPU的调度器可以立即切换到另一个就绪的线程(称为warp)继续执行。只要任何时候都有足够的可执行线程,GPU的计算单元就不会空闲。 +GPU则通过计算来隐藏内存延迟:当一个线程发起内存请求并等待数据返回时(这可能需要数百个时钟周期),GPU的调度器可以立即切换到另一个就绪的线程束(称为warp)继续执行。只要任何时候都有足够的可执行线程,GPU的计算单元就不会空闲。 CUDA Programming Guide对这个关键机制做了精确描述: @@ -612,7 +612,7 @@ __global__ void helloFromGPU() ``` - `__global__` 声明该函数是一个内核函数(Kernel Function) -- 内核函数的特点:在设备(GPU)上执行,但可以从主机(CPU)端调用 +- 内核函数的特点:在设备(GPU)上执行,可以从主机(CPU)端调用。 - 同一份内核代码会被N个CUDA线程"同时"并行执行 CUDA还提供了另外两个相关的函数说明符: diff --git "a/outputs/gpu-programming-course/docs/chapter10/\347\254\25410\347\253\240 \346\200\247\350\203\275\344\274\230\345\214\226\345\237\272\347\241\200\342\200\224\342\200\224\345\215\240\347\224\250\347\216\207\344\270\216\345\206\205\345\255\230\350\256\277\351\227\256\344\274\230\345\214\226.md" "b/outputs/gpu-programming-course/docs/chapter10/\347\254\25410\347\253\240 \346\200\247\350\203\275\344\274\230\345\214\226\345\237\272\347\241\200\342\200\224\342\200\224\345\215\240\347\224\250\347\216\207\344\270\216\345\206\205\345\255\230\350\256\277\351\227\256\344\274\230\345\214\226.md" index 7950cdb..88b8174 100644 --- "a/outputs/gpu-programming-course/docs/chapter10/\347\254\25410\347\253\240 \346\200\247\350\203\275\344\274\230\345\214\226\345\237\272\347\241\200\342\200\224\342\200\224\345\215\240\347\224\250\347\216\207\344\270\216\345\206\205\345\255\230\350\256\277\351\227\256\344\274\230\345\214\226.md" +++ "b/outputs/gpu-programming-course/docs/chapter10/\347\254\25410\347\253\240 \346\200\247\350\203\275\344\274\230\345\214\226\345\237\272\347\241\200\342\200\224\342\200\224\345\215\240\347\224\250\347\216\207\344\270\216\345\206\205\345\255\230\350\256\277\351\227\256\344\274\230\345\214\226.md" @@ -78,9 +78,8 @@ GPU 多处理器主要依赖线程级并行(Thread-Level Parallelism 隐藏 L 个时钟周期的延迟所需的指令数量取决于这些指令的吞吐量。假设指令以最大吞吐量执行: -- 对于 Compute Capability 5.x、6.1、6.2、7.x 和 8.x 的设备,需要 4L 条指令(一个多处理器每个时钟周期为4个 warp 各发射1条指令) -- 对于 Compute Capability 6.0 的设备,需要 2L 条指令 -- 对于 Compute Capability 3.x 的设备,需要 8L 条指令 +- 对于 Compute Capability 3.x、5.x、7.x 和 8.x 的设备,需要 4L 条指令(一个多处理器每个时钟周期为4个 warp 各发射1条指令) +- 对于 Compute Capability 6.0、6.1、6.2 的设备,需要 2L 条指令 延迟隐藏的核心思想是:当某些 warp 在等待内存访问结果时(延迟可能达到数百个时钟周期),其他 warp 可以继续执行。只要有足够多的活跃 warp,GPU 就能始终保持忙碌。 @@ -491,7 +490,7 @@ __global__ void transpose_naive(float *odata, float *idata, int width, int heigh - CC 3.x:共享内存有32个 Bank,每个 Bank 4字节宽。支持64位模式(8字节宽的 Bank)。 - CC 5.x / 6.x / 7.x / 8.x:共享内存有32个 Bank,每个时钟周期每个 Bank 32位带宽。这些架构也支持64位访问模式,使得 `double` 类型访问无 Bank Conflict。 -从 Compute Capability 5.0 开始,当 warp 内线程访问同一 Bank 内的不同地址时,如果所有访问都落在同一 Bank 的同一32位字内,则不会产生 Bank Conflict(这是广播机制)。 +从 Compute Capability 5.0 开始,当 warp 内线程访问同一 Bank 内的相同地址(即同一32位字),硬件会通过广播机制在单次访问内服务所有线程,则不会产生 Bank Conflict(这是广播机制)。 ### 10.4.5 常量内存(Constant Memory) diff --git "a/outputs/gpu-programming-course/docs/chapter11/\347\254\25411\347\253\240 \346\200\247\350\203\275\344\274\230\345\214\226\350\277\233\351\230\266\342\200\224\342\200\224\346\214\207\344\273\244\345\220\236\345\220\220\351\207\217\344\270\216Warp\347\272\247\344\274\230\345\214\226.md" "b/outputs/gpu-programming-course/docs/chapter11/\347\254\25411\347\253\240 \346\200\247\350\203\275\344\274\230\345\214\226\350\277\233\351\230\266\342\200\224\342\200\224\346\214\207\344\273\244\345\220\236\345\220\220\351\207\217\344\270\216Warp\347\272\247\344\274\230\345\214\226.md" index 77967ca..ea1b41f 100644 --- "a/outputs/gpu-programming-course/docs/chapter11/\347\254\25411\347\253\240 \346\200\247\350\203\275\344\274\230\345\214\226\350\277\233\351\230\266\342\200\224\342\200\224\346\214\207\344\273\244\345\220\236\345\220\220\351\207\217\344\270\216Warp\347\272\247\344\274\230\345\214\226.md" +++ "b/outputs/gpu-programming-course/docs/chapter11/\347\254\25411\347\253\240 \346\200\247\350\203\275\344\274\230\345\214\226\350\277\233\351\230\266\342\200\224\342\200\224\346\214\207\344\273\244\345\220\236\345\220\220\351\207\217\344\270\216Warp\347\272\247\344\274\230\345\214\226.md" @@ -41,7 +41,7 @@ 下表给出了各种算术指令的本机硬件支持吞吐量(每个时钟周期每个多处理器的结果数),数据来源于 CUDA 11.5.1 Programming Guide: -表 11.1 本机算术指令吞吐量(结果数/时钟周期/多处理器) +表 11.1 本机算术指令吞吐量(操作数/时钟周期/多处理器) | 指令类型 | 3.5/3.7 | 5.0/5.2 | 5.3 | 6.0 | 6.1/6.2 | 7.x | 8.0 | 8.6 | |---------|---------|---------|-----|-----|---------|-----|-----|-----| @@ -356,7 +356,7 @@ T __shfl_xor_sync(unsigned mask, T var, int laneMask, int width=warpSize); #### Shuffle 函数详解 -`__shfl_sync(mask, var, srcLane, width)`:返回由 `srcLane` 指定的 lane 持有的 `var` 值。如果 `width < warpSize`,每个子段表现为具有从0开始的逻辑 lane ID 的独立实体。如果 `srcLane` 超出 `[0:width-1]` 范围,返回的值对应于 `srcLane % width` 处的 lane 的值。 +`__shfl_sync(mask, var, srcLane, width)`:返回由 `srcLane` 指定的 lane 持有的 `var` 值。如果 `width < warpSize`,每个子段表现为具有从0开始的逻辑 lane ID 的独立实体。`srcLane` 必须在 `[0:width-1]` 范围内,否则返回值未定义。越界访问可能导致数据错误或程序崩溃,因此调用前务必保证 `srcLane` 合法。 ```cuda // 广播:lane 0 的值到整个 warp @@ -480,19 +480,21 @@ __global__ void reduce_smem(float *input, float *output, int n) { __shared__ float sdata[256]; int tid = threadIdx.x; int idx = threadIdx.x + blockIdx.x * blockDim.x; - + sdata[tid] = (idx < n) ? input[idx] : 0.0f; __syncthreads(); - + + // 跨 warp 归约 for (int s = blockDim.x / 2; s > 32; s >>= 1) { if (tid < s) sdata[tid] += sdata[tid + s]; __syncthreads(); } - - // 最后32个元素用 warp shuffle + + // 最后 64 个元素合并为 32 个,再使用 warp shuffle if (tid < 32) { - float val = sdata[tid]; + float val = sdata[tid] + sdata[tid + 32]; // 先合并 s=32 这一级 + __syncthreads(); // 理论上可省略,但保留以确保共享内存写入可见 for (int offset = 16; offset > 0; offset >>= 1) val += __shfl_xor_sync(0xffffffff, val, offset); if (tid == 0) diff --git "a/outputs/gpu-programming-course/docs/chapter2/\347\254\2542\347\253\240 CUDA\347\274\226\347\250\213\346\250\241\345\236\213\342\200\224\342\200\224\345\206\205\346\240\270\345\207\275\346\225\260\344\270\216\347\272\277\347\250\213\345\261\202\346\254\241\347\273\223\346\236\204.md" "b/outputs/gpu-programming-course/docs/chapter2/\347\254\2542\347\253\240 CUDA\347\274\226\347\250\213\346\250\241\345\236\213\342\200\224\342\200\224\345\206\205\346\240\270\345\207\275\346\225\260\344\270\216\347\272\277\347\250\213\345\261\202\346\254\241\347\273\223\346\236\204.md" index 153d4ca..a955684 100644 --- "a/outputs/gpu-programming-course/docs/chapter2/\347\254\2542\347\253\240 CUDA\347\274\226\347\250\213\346\250\241\345\236\213\342\200\224\342\200\224\345\206\205\346\240\270\345\207\275\346\225\260\344\270\216\347\272\277\347\250\213\345\261\202\346\254\241\347\273\223\346\236\204.md" +++ "b/outputs/gpu-programming-course/docs/chapter2/\347\254\2542\347\253\240 CUDA\347\274\226\347\250\213\346\250\241\345\236\213\342\200\224\342\200\224\345\206\205\346\240\270\345\207\275\346\225\260\344\270\216\347\272\277\347\250\213\345\261\202\346\254\241\347\273\223\346\236\204.md" @@ -111,7 +111,7 @@ float result = add(3.0f, 4.0f); // add() 函数体执行一次,result = 7.0f ``` -CUDA内核启动:函数体被N个线程各自独立执行。但所有线程接收相同的参数。区分不同线程"应该做什么"的唯一方式是使用内置变量(`threadIdx`等)。 +CUDA内核启动:函数体被N个线程各自独立执行。但所有线程接收相同的参数。区分不同线程"应该做什么"的最常用的方式是使用内置变量(`threadIdx`等)。 ```cuda __global__ void addVectors(float* A, float* B, float* C, int N) { @@ -163,7 +163,7 @@ int main() CUDA的线程层次结构是其可扩展性的基石。线程被组织为清晰的三级层次: 1. 线程(Thread):最细粒度的执行单元。每个线程有自己的寄存器和程序计数器。 -2. 线程块(Thread Block):一组可以彼此同步和共享数据的线程。线程块内的线程通过共享内存和`__syncthreads()`协作。 +2. 线程块(Thread Block):一组可以彼此同步和共享数据的线程。线程块内的线程通过共享内存和`__syncthreads()`协作;不同块间不同步,不能访问彼此共享内存。 3. 网格(Grid):组成一个内核启动的所有线程块。同一内核启动只能有一个网格。 CUDA Programming Guide对`threadIdx`使用三维向量的原因做了说明: @@ -323,7 +323,7 @@ CUDA Programming Guide对`blockIdx`和`blockDim`做了以下描述: > "Each block within the grid can be identified by a one-dimensional, two-dimensional, or three-dimensional unique index accessible within the kernel through the built-in blockIdx variable. The dimension of the thread block is accessible within the kernel through the built-in blockDim variable." > -> (网格中的每个线程块可以通过内置的`blockIdx`变量在内核中获取其一维、二维或三维的唯一索引。线程块的维度可以通过内置的`blockDim`变量在内核中获取。) +> (同一网格内的每个线程块,都拥有一个在该网格内唯一的一维、二维或三维索引;该索引可在内核函数内部通过内置变量 `blockIdx`获取。线程块的维度可以通过内置的`blockDim`变量在内核中获取。) ### 2.3.2 全局索引计算——CUDA编程最核心的公式 @@ -508,7 +508,7 @@ int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock; - N = 1000, threadsPerBlock = 256: `blocksPerGrid = (1000 + 256 - 1) / 256 = 1255 / 256 = 4` ✓ - N = 1024, threadsPerBlock = 256: - `blocksPerGrid = (1024 + 256 - 1) / 256 = 1279 / 256 = 4` ✓(恰好整除) + `blocksPerGrid = (1024 + 256 - 1) / 256 = 1279 / 256 = 4` ✓(恰好整除的时候,该公式依然有效) 二维情况: @@ -804,7 +804,7 @@ Done! ## 2.8 本章小结 -在本章中,我们深入探讨了CUDA编程模型的起两个支柱概念——内核函数和线程层次结构。我们的旅程涵盖了以下核心内容: +在本章中,我们深入探讨了CUDA编程模型的两个支柱概念——内核函数和线程层次结构。我们的旅程涵盖了以下核心内容: - 内核函数(Kernel):使用`__global__`声明说明符定义的函数,由N个CUDA线程并行执行N次。与普通函数"执行一次"的语义完全不同。通过`<<>>`执行配置语法指定线程的布局和数量。每个线程通过内置变量获取自己的唯一线程ID,从而知道"我负责处理哪个数据元素"。 diff --git "a/outputs/gpu-programming-course/docs/chapter3/\347\254\2543\347\253\240 CUDA\345\206\205\345\255\230\345\261\202\346\254\241\347\273\223\346\236\204\344\270\216\351\235\236\345\257\271\347\247\260\347\274\226\347\250\213.md" "b/outputs/gpu-programming-course/docs/chapter3/\347\254\2543\347\253\240 CUDA\345\206\205\345\255\230\345\261\202\346\254\241\347\273\223\346\236\204\344\270\216\351\235\236\345\257\271\347\247\260\347\274\226\347\250\213.md" index 33f63ea..511afb0 100644 --- "a/outputs/gpu-programming-course/docs/chapter3/\347\254\2543\347\253\240 CUDA\345\206\205\345\255\230\345\261\202\346\254\241\347\273\223\346\236\204\344\270\216\351\235\236\345\257\271\347\247\260\347\274\226\347\250\213.md" +++ "b/outputs/gpu-programming-course/docs/chapter3/\347\254\2543\347\253\240 CUDA\345\206\205\345\255\230\345\261\202\346\254\241\347\273\223\346\236\204\344\270\216\351\235\236\345\257\271\347\247\260\347\274\226\347\250\213.md" @@ -38,7 +38,7 @@ CUDA Programming Guide还提到了两个额外的只读内存空间: 寄存器的关键特性: - 作用域:单个线程(线程私有,其他线程无法访问) - 访问延迟:零延迟——编译器可以直接将寄存器作为指令的操作数 -- 容量:每个线程最多可以使用最多255个寄存器(具体上限取决于计算能力)。超过上限的变量会被编译器"溢出"到本地内存中 +- 容量:每个线程最多可以使用255个寄存器(具体上限取决于计算能力)。超过上限的变量会被编译器"溢出"到本地内存中 - 典型用途:频繁使用的标量变量、循环计数器、累加器、中间计算结果 在内核函数中声明的所有局部变量(标量类型,如 `int`, `float`, `double`)默认都试图放入寄存器中。CUDA编译器在编译时会进行寄存器分配——将最常用的变量保留在寄存器中,将使用频率较低的变量溢出到本地内存。 diff --git "a/outputs/gpu-programming-course/docs/chapter4/\347\254\2544\347\253\240 NVCC\347\274\226\350\257\221\346\250\241\345\236\213.md" "b/outputs/gpu-programming-course/docs/chapter4/\347\254\2544\347\253\240 NVCC\347\274\226\350\257\221\346\250\241\345\236\213.md" index 9229fb3..8a02a1a 100644 --- "a/outputs/gpu-programming-course/docs/chapter4/\347\254\2544\347\253\240 NVCC\347\274\226\350\257\221\346\250\241\345\236\213.md" +++ "b/outputs/gpu-programming-course/docs/chapter4/\347\254\2544\347\253\240 NVCC\347\274\226\350\257\221\346\250\241\345\236\213.md" @@ -263,14 +263,19 @@ ls -la ~/.nv/ComputeCache/ 二进制代码是体系结构相关的。一个 cubin 对象通过编译器选项 `-code` 指定目标架构来生成。例如,使用 `-code=sm_35` 编译会生成针对计算能力(Compute Capability) 3.5 的设备的二进制代码。 -二进制兼容性遵循单主版本内、向前兼容的规则: +二进制兼容性规则: +cubin遵循单主版本内、向前兼容的规则: > 为计算能力 X.y 生成的 cubin 对象仅能在计算能力 X.z(其中 z >= y)的设备上执行。 +PTX + JIT 兼容性规则: +PTX 虚拟指令集遵循跨主版本向前兼容规则: +为计算能力 X.y 生成的 PTX 代码,可以在所有计算能力≥X.y 的设备上通过 JIT 编译执行。 + 具体来说: -- 向前兼容(同一个主版本 X 内):`sm_35` 可以在 `sm_37` 上运行。`sm_50` 可以在 `sm_52`、`sm_53` 上运行。`sm_80` 可以在 `sm_86`、`sm_89` 上运行。 -- 不支持向后兼容:`sm_80` 不能在 `sm_75` 上运行。 -- 不支持跨主版本兼容:`sm_35` 不能在 `sm_50` 上运行(因为主版本 3 ≠ 5)。`sm_60` 不能在 `sm_70` 上运行(因为主版本 6 ≠ 7)。 +- 向前兼容(同一个主版本 X 内):`compute_35` 可以在 `sm_37` 上运行。`compute_50` 可以在 `sm_52`、`sm_53` 上运行。`compute_80` 可以在 `sm_86`、`sm_89` 上运行。 +- 不支持向后兼容:`compute_80` 不能在 `sm_75` 上运行。 +- 不支持跨主版本兼容:`compute_35` 不能在 `sm_50` 上运行(因为主版本 3 ≠ 5)。`compute_60` 不能在 `sm_70` 上运行(因为主版本 6 ≠ 7)。 > 注意:二进制兼容性仅支持桌面平台(Desktop)。Tegra 平台不支持二进制兼容性。桌面与 Tegra 之间的二进制兼容性也不支持。 @@ -671,7 +676,7 @@ NVCC 对 64 位和 32 位编译模式有明确的规则: - 32 位版本的 NVCC 可以使用 `-m64` 选项在 64 位模式下编译设备代码。 - 64 位版本的 NVCC 可以使用 `-m32` 选项在 32 位模式下编译设备代码。 -> 提示:在现代 CUDA 开发中(CUDA 10.0 及以后),NVIDIA 已不再提供 32 位版本的 CUDA Toolkit。所有开发都应在 64 位模式下进行。除非你在维护非常老旧的遗留系统,否则不需要关心 32 位模式。 +> 提示:在现代 CUDA 开发中,NVIDIA 已在 CUDA 11.0 开始逐步废弃 32 位主机应用支持,CUDA 12.x 已完全移除 32 位编译功能。所有开发都应在 64 位模式下进行。除非你在维护非常老旧的遗留系统,否则不需要关心 32 位模式。 ### 4.8.3 独立编译与分离编译 @@ -869,6 +874,69 @@ nvcc file.cu -o file -gencode ... -gencode ... -gencode ... CUDA 编译器需要为每个 `-gencode` 选项重新编译设备代码。将大型设备函数放在 `.cuh` 头文件中会导致它们在每个翻译单元中被重复编译。尽可能将设备代码放在 `.cu` 文件中,只将简洁的接口声明放在头文件中。 +### 4.8.6 CUDA Driver API 编译与运行时加载 + +虽然大多数 CUDA 应用使用 Runtime API(`cuda*` 函数和自动 Fat Binary 管理),但了解 Driver API(`cu*` 函数)的编译模型也很重要。Driver API 提供了更精细的控制: + +编译为独立的 cubin 文件: + +```bash +# 编译设备代码为独立 cubin(不包含主机代码) +nvcc kernel.cu -cubin -arch=sm_80 -o kernel.cubin + +# 编译设备代码为 PTX(用于 JIT) +nvcc kernel.cu -ptx -arch=compute_80 -o kernel.ptx + +# 或使用 Fat Binary 但保存为独立文件 +nvcc kernel.cu -fatbin -arch=sm_80 -o kernel.fatbin +``` + +在 Driver API 中加载 cubin: + +```cuda +#include // Driver API 头文件 + +// 初始化 Driver API(必须!) +cuInit(0); + +// 获取设备 +CUdevice device; +cuDeviceGet(&device, 0); + +// 创建上下文 +CUcontext context; +cuCtxCreate(&context, 0, device); + +// 从文件加载 cubin 或 PTX 模块 +CUmodule module; +cuModuleLoad(&module, "kernel.cubin"); +// 或加载 PTX:cuModuleLoad(&module, "kernel.ptx"); + +// 获取内核函数句柄 +CUfunction kernel; +cuModuleGetFunction(&kernel, module, "_Z8myKernelPf"); + +// 设置参数并启动 +void *args[] = { &d_data, &N }; +cuLaunchKernel(kernel, + gridDimX, gridDimY, 1, // grid 维度 + blockDimX, 1, 1, // block 维度 + 0, // 共享内存 + NULL, // 流 + args, NULL); // 参数 + +// 清理 +cuCtxDestroy(context); +``` + +Driver API 的优势是精确控制——你可以: +- 在运行时选择加载哪个 .cubin 文件(基于设备检测)。 +- 看 JIT 编译确切的 PTX 代码。 +- 管理多个 CUDA 上下文。 +- 实现自定义的内核缓存和加载策略。 + +> 提示:Runtime API 在内部就是通过 Driver API 实现的。使用 Runtime API 时,NVCC 自动生成的代码本质上就是在执行类似上面 `cuModuleLoad` → `cuModuleGetFunction` → `cuLaunchKernel` 的操作。 + ## 4.9 构建系统集成与实战 在真实项目中,CUDA 代码很少单独使用 `nvcc` 命令行编译,而是集成在构建系统中。本节介绍如何将 CUDA 编译集成到 CMake 和 Makefile 中。 @@ -983,7 +1051,6 @@ CUDA Toolkit 在运行时受多种环境变量的影响。以下是开发中常 | `CUDA_CACHE_DISABLE` | 设为 1 禁用 JIT 缓存 | `CUDA_CACHE_DISABLE=1` | | `CUDA_CACHE_PATH` | JIT 缓存目录 | `CUDA_CACHE_PATH=/tmp/cuda_cache` | | `CUDA_DEVICE_MAX_CONNECTIONS` | 每个设备的 CUDA 流多路复用能力 | 默认 8 | -| `CUDA_ERROR_CHECKING` | 控制运行时错误检查粒度 | 默认中等 | | `CUDA_MANAGED_FORCE_DEVICE_ALLOC` | 强制统一内存分配在设备端 | | 在开发中常用环境变量组合: @@ -1010,68 +1077,6 @@ CUDA_CACHE_DISABLE=1 ./my_app | 极致性能 | 每架构单独编译分发 | 超算/HPC 环境 | | 动态选择 | Driver API 运行时加载 | 框架类库 | -### 4.8.6 CUDA Driver API 编译与运行时加载 - -虽然大多数 CUDA 应用使用 Runtime API(`cuda*` 函数和自动 Fat Binary 管理),但了解 Driver API(`cu*` 函数)的编译模型也很重要。Driver API 提供了更精细的控制: - -编译为独立的 cubin 文件: - -```bash -# 编译设备代码为独立 cubin(不包含主机代码) -nvcc kernel.cu -cubin -arch=sm_80 -o kernel.cubin - -# 编译设备代码为 PTX(用于 JIT) -nvcc kernel.cu -ptx -arch=compute_80 -o kernel.ptx - -# 或使用 Fat Binary 但保存为独立文件 -nvcc kernel.cu -fatbin -arch=sm_80 -o kernel.fatbin -``` - -在 Driver API 中加载 cubin: - -```cuda -#include // Driver API 头文件 - -// 初始化 Driver API(必须!) -cuInit(0); - -// 获取设备 -CUdevice device; -cuDeviceGet(&device, 0); - -// 创建上下文 -CUcontext context; -cuCtxCreate(&context, 0, device); - -// 从文件加载 cubin 或 PTX 模块 -CUmodule module; -cuModuleLoad(&module, "kernel.cubin"); -// 或加载 PTX:cuModuleLoad(&module, "kernel.ptx"); - -// 获取内核函数句柄 -CUfunction kernel; -cuModuleGetFunction(&kernel, module, "_Z8myKernelPf"); - -// 设置参数并启动 -void *args[] = { &d_data, &N }; -cuLaunchKernel(kernel, - gridDimX, gridDimY, 1, // grid 维度 - blockDimX, 1, 1, // block 维度 - 0, // 共享内存 - NULL, // 流 - args, NULL); // 参数 - -// 清理 -cuCtxDestroy(context); -``` - -Driver API 的优势是精确控制——你可以: -- 在运行时选择加载哪个 .cubin 文件(基于设备检测)。 -- 看 JIT 编译确切的 PTX 代码。 -- 管理多个 CUDA 上下文。 -- 实现自定义的内核缓存和加载策略。 - -> 提示:Runtime API 在内部就是通过 Driver API 实现的。使用 Runtime API 时,NVCC 自动生成的代码本质上就是在执行类似上面 `cuModuleLoad` → `cuModuleGetFunction` → `cuLaunchKernel` 的操作。 ## 4.10 动手体验:编译和观察 Fat Binary @@ -1289,7 +1294,7 @@ ls -la device_query* - `-arch` 和 `-code` 的关系? `-arch=compute_80` 指定 PTX 功能级别。`-code=sm_80` 指定 cubin 目标。简写 `-arch=sm_80` 等价于 `-arch=compute_80 -code=compute_80,sm_80`,同时嵌入了 cubin 和 PTX。 -- C++ 兼容性和 64 位支持? 主机代码完全支持 C++。设备代码仅支持 C++ 的一个子集(不支持 STL、异常和 RTTI)。现代 CUDA 开发均应在 64 位模式下进行(32 位支持已在 CUDA 10.0 后移除)。 +- C++ 兼容性和 64 位支持? 主机代码完全支持 C++。设备代码仅支持 C++ 的一个子集(不支持标准 C++ 异常和 RTTI,仅支持有限的 STL 子集)。现代 CUDA 开发均应在 64 位模式下进行(32 位主机应用支持已在 CUDA 11.0 开始逐步废弃,CUDA 12.x 已完全移除 32 位支持)。 通过本章的学习,我们建立了对 CUDA 编译模型的系统理解。在下一章中,我们将进入 CUDA 运行时(CUDA Runtime)的世界,学习设备内存管理——如何在代码中分配和释放 GPU 内存,以及如何在主机和设备之间传输数据。这是编写任何 CUDA 程序都必不可少的基础技能! diff --git "a/outputs/gpu-programming-course/docs/chapter5/\347\254\2545\347\253\240 CUDA\350\277\220\350\241\214\346\227\266\342\200\224\342\200\224\350\256\276\345\244\207\345\206\205\345\255\230\347\256\241\347\220\206.md" "b/outputs/gpu-programming-course/docs/chapter5/\347\254\2545\347\253\240 CUDA\350\277\220\350\241\214\346\227\266\342\200\224\342\200\224\350\256\276\345\244\207\345\206\205\345\255\230\347\256\241\347\220\206.md" index db81001..ea48822 100644 --- "a/outputs/gpu-programming-course/docs/chapter5/\347\254\2545\347\253\240 CUDA\350\277\220\350\241\214\346\227\266\342\200\224\342\200\224\350\256\276\345\244\207\345\206\205\345\255\230\347\256\241\347\220\206.md" +++ "b/outputs/gpu-programming-course/docs/chapter5/\347\254\2545\347\253\240 CUDA\350\277\220\350\241\214\346\227\266\342\200\224\342\200\224\350\256\276\345\244\207\345\206\205\345\255\230\347\256\241\347\220\206.md" @@ -14,7 +14,7 @@ | 链接方式 | Linux 库文件 | Windows 库文件 | 特点 | | :--- | :--- | :--- | :--- | -| 静态链接 | `libcudart.a` | `cudart.lib` | 运行时代码嵌入可执行文件,无需附带 DLL | +| 静态链接 | `libcudart_static.a` | `cudart_static.lib` | 运行时代码嵌入可执行文件,无需附带 DLL | | 动态链接 | `libcudart.so` | `cudart.dll` | 可执行文件更小,运行时需要 DLL/SO 可用 | diff --git "a/outputs/gpu-programming-course/docs/chapter6/\347\254\2546\347\253\240 \345\205\261\344\272\253\345\206\205\345\255\230\344\270\216\351\241\265\351\224\201\345\256\232\344\270\273\346\234\272\345\206\205\345\255\230.md" "b/outputs/gpu-programming-course/docs/chapter6/\347\254\2546\347\253\240 \345\205\261\344\272\253\345\206\205\345\255\230\344\270\216\351\241\265\351\224\201\345\256\232\344\270\273\346\234\272\345\206\205\345\255\230.md" index 6758b72..217bc36 100644 --- "a/outputs/gpu-programming-course/docs/chapter6/\347\254\2546\347\253\240 \345\205\261\344\272\253\345\206\205\345\255\230\344\270\216\351\241\265\351\224\201\345\256\232\344\270\273\346\234\272\345\206\205\345\255\230.md" +++ "b/outputs/gpu-programming-course/docs/chapter6/\347\254\2546\347\253\240 \345\205\261\344\272\253\345\206\205\345\255\230\344\270\216\351\241\265\351\224\201\345\256\232\344\270\273\346\234\272\345\206\205\345\255\230.md" @@ -716,7 +716,7 @@ float *d_data; cudaHostGetDevicePointer(&d_data, h_data, 0); ``` -> 注意:如果主机和设备使用统一地址空间(UVA),且内存是通过 `cudaHostAlloc()` 分配的,那么主机指针和设备指针是同一个值。此时不需要、也不应该调用 `cudaHostGetDevicePointer()`(调用会返回 `cudaErrorInvalidValue`)。 +> 注意:如果主机和设备使用统一地址空间(UVA),且内存是通过 `cudaHostAlloc()` 分配的,那么主机指针和设备指针是同一个值。如果确定 UVA 已启用且内存被映射,但为保持代码的兼容性和清晰性,建议仍然调用 cudaHostGetDevicePointer()——该调用在 UVA 下也会成功,且不会产生额外开销。 启用映射内存支持: diff --git "a/outputs/gpu-programming-course/docs/chapter7/# \347\254\2547\347\253\240 \346\265\201\343\200\201\344\272\213\344\273\266\344\270\216\345\274\202\346\255\245\345\271\266\345\217\221\346\211\247\350\241\214" "b/outputs/gpu-programming-course/docs/chapter7/# \347\254\2547\347\253\240 \346\265\201\343\200\201\344\272\213\344\273\266\344\270\216\345\274\202\346\255\245\345\271\266\345\217\221\346\211\247\350\241\214" new file mode 100644 index 0000000..23b271b --- /dev/null +++ "b/outputs/gpu-programming-course/docs/chapter7/# \347\254\2547\347\253\240 \346\265\201\343\200\201\344\272\213\344\273\266\344\270\216\345\274\202\346\255\245\345\271\266\345\217\221\346\211\247\350\241\214" @@ -0,0 +1,805 @@ +# 第7章 流、事件与异步并发执行 + +## 7.1 引言 + +在前面的章节中,我们学习了如何编写 CUDA 核函数并完成主机与设备之间的数据传输。到目前为止,我们的程序都是按顺序执行的:主机发起一个操作,等待它完成,然后再发起下一个操作。这种方式虽然简单,但远未充分利用 GPU 的并行能力。 + +现代 NVIDIA GPU 具备同时执行多个操作的能力——例如,在运行一个核函数的同时传输另一批数据,甚至同时运行多个核函数。要解锁这种并发能力,我们需要掌握 CUDA 中两个至关重要的概念:流(Stream)事件(Event)。 + +本章将带你从零开始,逐步理解和掌握 CUDA 的异步并发执行模型。你将学会如何创建和使用流来管理并发操作,如何利用事件来进行精确的时间测量和同步,以及如何在实际项目中通过流重叠数据传输与计算来显著提升程序性能。 + +## 7.2 异步并发执行概述 + +CUDA 将以下操作暴露为可以彼此并发执行的独立任务(CUDA Programming Guide 3.2.6): + +1. 主机上的计算(Computation on the host); +2. 设备上的计算(Computation on the device); +3. 从主机到设备的内存传输(Memory transfers from host to device); +4. 从设备到主机的内存传输(Memory transfers from device to host); +5. 给定设备内部的内存传输(Memory transfers within a given device); +6. 设备之间的内存传输(Memory transfers among devices)。 + +这些操作之间能达到的并发程度取决于设备的特性集和计算能力(compute capability)。下面我们逐一了解各种并发场景。 + +### 7.2.1 主机与设备之间的并发执行 + +CUDA 通过异步库函数(asynchronous library functions)来促进主机与设备的并发执行。这些函数在设备完成任务之前就将控制权返回给主机线程。借助异步调用,许多设备操作可以被排入队列,由 CUDA 驱动在合适的设备资源可用时执行。这样主机线程就从管理设备的大量职责中解脱出来,可以自由地处理其他任务。 + +以下设备操作相对于主机是异步的: + +- 核函数启动(Kernel launches); +- 使用cudaMemcpyAsync的单个设备内部的内存拷贝; +- 从页锁定主机内存到设备的内存拷贝(大小不限); +- 以 `Async` 为后缀的内存拷贝函数; +- 内存设置函数调用(memory set function calls)。 + +程序员可以通过设置环境变量 `CUDA_LAUNCH_BLOCKING` 为 1 来在全局范围内禁用所有 CUDA 应用程序核函数启动的异步性。这个特性仅为调试目的提供,不应作为使生产软件可靠运行的手段。 + +此外,当通过性能分析器(如 Nsight、Visual Profiler)收集硬件计数器时,核函数启动会变为同步的,除非启用了并发核函数分析(concurrent kernel profiling)。如果涉及的内存不是页锁定(page-locked)内存,`Async` 内存拷贝也会变为同步。 + +### 7.2.2 并发核函数执行 + +计算能力 2.x 及以上的某些设备可以同时执行多个核函数。应用程序可以通过检查 `concurrentKernels` 设备属性来查询这一能力,如果设备支持该特性,该属性值为 1。 + +一个设备能够并发执行的核函数启动的最大数量取决于其计算能力。但需要注意: +- 来自一个 CUDA 上下文的核函数不能与来自另一个 CUDA 上下文的核函数并发执行; +- 使用大量纹理或大量局部内存的核函数不太可能与其他核函数并发执行。 + +### 7.2.3 数据传输与核函数执行的重叠 + +某些设备可以在执行核函数的同时进行异步内存拷贝。应用程序可以通过检查 `asyncEngineCount` 设备属性来查询这一能力,对于支持该特性的设备,该值大于零。如果拷贝涉及主机内存,则主机内存必须是页锁定的。 + +也可以在核函数执行的同时进行设备内部拷贝(对于支持 `concurrentKernels` 设备属性的设备)和/或与设备的拷贝操作同时进行(对于支持 `asyncEngineCount` 属性的设备)。 + +### 7.2.4 并发数据传输 + +计算能力 2.x 及以上的某些设备可以将从设备拷入和拷出的操作重叠执行。应用程序可以通过检查 `asyncEngineCount` 设备属性来查询这一能力,对于支持双向并发传输的设备,该值等于 2。为了实现重叠,传输中涉及的任何主机内存必须是页锁定的。 + +## 7.3 流的详解 + +### 7.3.1 什么是流 + +应用程序通过流(stream)来管理上述并发操作。流是一系列按顺序执行的命令(可能由不同的主机线程发起)。不同的流则可以彼此乱序或并发地执行它们的命令;但此行为不被保证,因此不应依赖它来确保正确性(例如,核函数间通信是未定义的)。 + +在流上发出的命令可能在其所有依赖都被满足时执行。依赖可以是同一流上先前启动的命令,也可以是来自其他流的依赖。同步调用成功完成可保证所有已启动的命令均已完成。 + +### 7.3.2 流的创建与销毁 + +流对象在使用前需要创建,使用后需要销毁。以下代码展示了两个流的创建: + +```cuda +cudaStream_t stream[2]; +for (int i = 0; i < 2; ++i) + cudaStreamCreate(&stream[i]); +float* hostPtr; +cudaMallocHost(&hostPtr, 2 * size); +``` + +每个流可以定义为一组操作的序列。以下代码为每个流定义了一个从主机到设备的内存拷贝、一个核函数启动和一个从设备到主机的内存拷贝: + +```cuda +for (int i = 0; i < 2; ++i) { + cudaMemcpyAsync(inputDevPtr + i * size, hostPtr + i * size, + size, cudaMemcpyHostToDevice, stream[i]); + MyKernel <<<100, 512, 0, stream[i]>>> + (outputDevPtr + i * size, inputDevPtr + i * size, size); + cudaMemcpyAsync(hostPtr + i * size, outputDevPtr + i * size, + size, cudaMemcpyDeviceToHost, stream[i]); +} +``` + +这里,每个流拷贝输入数组 `hostPtr` 的一部分到设备内存中的 `inputDevPtr`,通过调用 `MyKernel()` 在设备上处理 `inputDevPtr`,然后将结果 `outputDevPtr` 拷贝回 `hostPtr` 的对应部分。 + +流通过调用 `cudaStreamDestroy()` 来释放: + +```cuda +for (int i = 0; i < 2; ++i) + cudaStreamDestroy(stream[i]); +``` + +如果调用 `cudaStreamDestroy()` 时设备仍在流中执行工作,该函数将立即返回,与流关联的资源会在设备完成流中所有工作后自动释放。 + +这里有个重要提醒:上述示例中 `hostPtr` 必须指向页锁定(pinned)主机内存,任何重叠才能发生。这是因为非页锁定内存的拷贝总是同步的。 + +### 7.3.3 默认流与每线程默认流 + +核函数启动和主机与设备之间的内存拷贝,如果没有指定任何流参数,或者等价地将流参数设为零,会被发到默认流(default stream)中。因此,它们在默认流中是按顺序执行的。 + +根据编译选项的不同,默认流有两种行为模式: + +(1)Legacy 默认流(NULL 流) + +使用 `--default-stream legacy` 编译标志时(这也是不指定任何 `--default-stream` 标志时的默认行为),默认流是一个称为 NULL 流(NULL stream)的特殊流,每个设备只有一个 NULL 流,供所有主机线程使用。NULL 流的特殊之处在于它会引发隐式同步(implicit synchronization)。 + +(2)每线程默认流(per-thread default stream) + +使用 `--default-stream per-thread` 编译标志编译的代码(或者在包含 CUDA 头文件之前定义 `CUDA_API_PER_THREAD_DEFAULT_STREAM` 宏),默认流是一个常规流,每个主机线程都有自己的默认流。 + +需要注意的是,如果使用 `nvcc` 编译,`#define CUDA_API_PER_THREAD_DEFAULT_STREAM 1` 不能用来启用此行为,因为 `nvcc` 会在翻译单元顶部隐式包含 `cuda_runtime.h`。在这种情况下,需要使用 `--default-stream per-thread` 编译标志,或者用 `-DCUDA_API_PER_THREAD_DEFAULT_STREAM=1` 编译器标志定义该宏。 + +### 7.3.4 显式同步 + +有多种方式可以显式地同步流: + +- `cudaDeviceSynchronize()`:等待当前主机线程在当前设备上发起的所有流中的所有先前命令完成。这是最"重"的同步方式,会阻塞主机直到整个 GPU 上的所有工作完成。 + +- `cudaStreamSynchronize(stream)`:接受一个流作为参数,等待该给定流中的所有先前命令完成。它可以用于将主机与一个特定流同步,同时允许其他流继续在设备上执行。这是更精细的同步方式。 + +- `cudaStreamWaitEvent(stream, event)`:接受一个流和一个事件作为参数,使得在调用 `cudaStreamWaitEvent()` 之后添加到给定流的所有命令,延迟其执行直到给定事件完成。这允许跨流的同步。 + +- `cudaStreamQuery(stream)`:为应用程序提供了一种了解流中所有先前命令是否已完成的方法。它不会阻塞,如果流中所有操作已完成则返回 `cudaSuccess`,否则返回 `cudaErrorNotReady`。 + +### 7.3.5 隐式同步 + +如果主机线程在两条不同流的命令之间发出了以下任何操作,这两个命令就不能并发运行: + +- 页锁定主机内存分配(page-locked host memory allocation); +- 设备内存分配(device memory allocation); +- 设备内存设置(device memory set); +- 同步的同一设备内存上两个地址之间的内存拷贝; +- 发往 NULL 流的任何 CUDA 命令; +- L1/共享内存配置的切换。 + +对于支持并发核函数执行且计算能力为 3.0 或更低的设备,任何需要依赖检查以查看流式核函数启动是否完成的操作: +- 只能在 CUDA 上下文中所有先前来自任何流的核函数启动的所有线程块都已开始执行后,才能开始执行; +- 会阻塞 CUDA 上下文中任何流的所有后续核函数启动,直到被检查的核函数启动完成。 + +需要依赖检查的操作包括与被检查启动相同流内的任何其他命令,以及对该流的任何 `cudaStreamQuery()` 调用。因此,应用程序应遵循以下准则以提高并发核函数执行的潜力: +- 所有独立操作应在依赖操作之前发出; +- 任何类型的同步都应尽可能延迟。 + +### 7.3.6 流重叠行为 + +两个流之间执行重叠的程度取决于命令向每个流发出的顺序,以及设备是否支持数据传输与核函数执行的重叠、并发核函数执行和/或并发数据传输。 + +考虑 7.3.2 节的代码示例(在每个流内按 H2D -> Kernel -> D2H 顺序发出)。在不支持并发数据传输的设备上,两个流根本不会重叠——因为发往 `stream[1]` 的主机到设备内存拷贝是在发往 `stream[0]` 的设备到主机内存拷贝之后发出的,所以它只能在 `stream[0]` 的设备到主机拷贝完成后才能开始,而后者必须等待 `stream[0]` 的核函数完成,因此实际上完全串行化了。 + +如果代码改写为以下方式(将相同类型的操作分组): + +```cuda +for (int i = 0; i < 2; ++i) + cudaMemcpyAsync(inputDevPtr + i * size, hostPtr + i * size, + size, cudaMemcpyHostToDevice, stream[i]); +for (int i = 0; i < 2; ++i) + MyKernel <<<100, 512, 0, stream[i]>>> + (outputDevPtr + i * size, inputDevPtr + i * size, size); +for (int i = 0; i < 2; ++i) + cudaMemcpyAsync(hostPtr + i * size, outputDevPtr + i * size, + size, cudaMemcpyDeviceToHost, stream[i]); +``` + +那么发往 `stream[1]` 的主机到设备内存拷贝就可以与发往 `stream[0]` 的核函数启动重叠。 + +在支持并发数据传输的设备上,7.3.2 节的原始代码示例中的两个流确实会重叠:发往 `stream[1]` 的主机到设备内存拷贝与发往 `stream[0]` 的设备到主机内存拷贝重叠,甚至与发往 `stream[0]` 的核函数启动重叠(假设设备支持数据传输与核函数执行的重叠)。但对于计算能力 3.0 或更低的设备,由于隐式同步的限制,核函数执行之间不可能重叠。 + +这个分析揭示了一个重要的编程原则:要最大化并发,应当在发出依赖操作之前先发出所有独立操作,并且尽量将相同方向的数据传输集中发出。 + +## 7.4 事件详解 + +### 7.4.1 事件的创建与销毁 + +运行时还提供了一种密切监控设备进度以及执行精确计时的方法,即让应用程序在程序中的任何点异步记录事件(event),并查询这些事件何时完成。当事件之前的所有任务——或可选地,给定流中的所有命令——都已完成时,该事件即被视为完成。在流 0(即默认流)中记录的事件,在所有流中的所有先前任务和命令完成后才被视为完成。 + +以下代码创建两个事件: + +```cuda +cudaEvent_t start, stop; +cudaEventCreate(&start); +cudaEventCreate(&stop); +``` + +事件通过以下方式销毁: + +```cuda +cudaEventDestroy(start); +cudaEventDestroy(stop); +``` + +### 7.4.2 使用事件测量执行时间 + +事件最常见的用途之一是测量 GPU 操作的执行时间。以下代码展示了如何使用事件来计时一段 CUDA 操作: + +```cuda +cudaEventRecord(start, 0); +for (int i = 0; i < 2; ++i) { + cudaMemcpyAsync(inputDev + i * size, inputHost + i * size, + size, cudaMemcpyHostToDevice, stream[i]); + MyKernel <<<100, 512, 0, stream[i]>>> + (outputDev + i * size, inputDev + i * size, size); + cudaMemcpyAsync(outputHost + i * size, outputDev + i * size, + size, cudaMemcpyDeviceToHost, stream[i]); +} +cudaEventRecord(stop, 0); +cudaEventSynchronize(stop); +float elapsedTime; +cudaEventElapsedTime(&elapsedTime, start, stop); +``` + +这里 `cudaEventRecord()` 将事件记录到指定的流中。当该事件之前的所有操作都完成后,事件就完成了。`cudaEventSynchronize()` 阻塞主机直到事件完成,然后 `cudaEventElapsedTime()` 计算两个事件之间的时间间隔(以毫秒为单位)。 + +### 7.4.3 事件的同步功能 + +除了计时,事件也是流之间同步的重要工具。`cudaStreamWaitEvent()` 函数使得一个流可以等待另一个流中记录的事件。这是一种强大且灵活的跨流同步机制。 + +例如,如果我们想让 `stream[1]` 中的核函数在 `stream[0]` 中的某个特定点之后才开始执行,我们可以这样做: + +```cuda +cudaEvent_t syncEvent; +cudaEventCreate(&syncEvent); + +// 在 stream[0] 中记录事件 +cudaMemcpyAsync(d_a, h_a, size, cudaMemcpyHostToDevice, stream[0]); +cudaEventRecord(syncEvent, stream[0]); // 标记点 + +// stream[1] 等待这个事件 +cudaStreamWaitEvent(stream[1], syncEvent, 0); +// stream[1] 的后续操作会等到 stream[0] 中 syncEvent 之前的所有操作完成 +MyKernel <<<..., stream[1]>>>(...); +``` + +## 7.5 流优先级 + +流的相对优先级可以在创建时通过 `cudaStreamCreateWithPriority()` 指定。允许的优先级范围(按从最高优先级到最低优先级排列)可以通过 `cudaDeviceGetStreamPriorityRange()` 函数获取。在运行时,高优先级流中的待处理工作优先于低优先级流中的待处理工作。 + +以下代码示例获取当前设备允许的优先级范围,并创建具有最高和最低可用优先级的流: + +```cuda +// 获取此设备的流优先级范围 +int priority_high, priority_low; +cudaDeviceGetStreamPriorityRange(&priority_low, &priority_high); +// 创建具有最高和最低可用优先级的流 +cudaStream_t st_high, st_low; +cudaStreamCreateWithPriority(&st_high, cudaStreamNonBlocking, priority_high); +cudaStreamCreateWithPriority(&st_low, cudaStreamNonBlocking, priority_low); +``` + +需要注意几点: +- `cudaStreamCreateWithPriority` 的第二个参数是标志位,`cudaStreamNonBlocking` 表示创建的流不会与 NULL 流同步。 +- 优先级数值的含义:`priority_low` 和 `priority_high` 分别代表数值上的最低和最高。最高优先级的流中的工作会优先被调度。 +- 流优先级是一个提示(hint)而非硬性保证,GPU 调度器会尽力满足但不能保证严格按优先级调度。 + +## 7.6 主机函数(回调) + +运行时提供了一种在流中的任意点插入 CPU 函数调用的方法,通过 `cudaLaunchHostFunc()` 实现。提供的函数在流中回调之前的所有命令都完成后在主机上执行。 + +以下代码将主机函数 `MyCallback` 添加到两个流中,放在主机到设备内存拷贝、核函数启动和设备到主机内存拷贝之后: + +```cuda +void CUDART_CB MyCallback(cudaStream_t stream, cudaError_t status, void *data){ + printf("Inside callback %d\n", (size_t)data); +} +... +for (size_t i = 0; i < 2; ++i) { + cudaMemcpyAsync(devPtrIn[i], hostPtr[i], size, cudaMemcpyHostToDevice, stream[i]); + MyKernel <<<100, 512, 0, stream[i]>>>(devPtrOut[i], devPtrIn[i], size); + cudaMemcpyAsync(hostPtr[i], devPtrOut[i], size, cudaMemcpyDeviceToHost, stream[i]); + cudaLaunchHostFunc(stream[i], MyCallback, (void*)i); +} +``` + +关于回调有两条重要规则: + +1. 在流中主机函数之后发出的命令,在该函数完成之前不会开始执行。这意味着回调会阻塞该流中后续操作的启动,但不会影响其他流。 + +2. 排入流中的主机函数不得直接或间接地进行 CUDA API 调用,因为如果它进行此类调用,可能会最终等待自己,导致死锁。 + +回调的典型使用场景包括: +- 在 GPU 完成某一阶段的计算后触发 CPU 端的后续处理; +- 在数据传输完成后释放或重用主机内存; +- 实现复杂的流水线控制逻辑。 + +## 7.7 动手体验:多流流水线数据传输 + +下面我们通过一个完整的可运行示例,演示如何使用多流实现数据传输与核函数执行的流水线重叠。这个示例将数据处理分为多个阶段,使用两个流以流水线方式交替执行,从而隐藏数据传输延迟。 + +### 7.7.1 完整代码 + +将以下代码保存为 `pipeline_streams.cu`: + +```cuda +#include +#include + +// 简单的向量加法核函数 +__global__ void vectorAdd(const float* A, const float* B, float* C, int N) { + int i = blockIdx.x * blockDim.x + threadIdx.x; + if (i < N) { + C[i] = A[i] + B[i]; + } +} + +// 初始化主机数据 +void initData(float* data, int N, float val) { + for (int i = 0; i < N; i++) { + data[i] = val; + } +} + +// 验证结果 +bool verifyResult(const float* data, int N, float expected) { + for (int i = 0; i < N; i++) { + if (fabs(data[i] - expected) > 1e-5) { + printf("Mismatch at index %d: expected %f, got %f\n", i, expected, data[i]); + return false; + } + } + return true; +} + +int main() { + // 参数设置 + const int N = 1 << 22; // 约 4M 元素,16MB + const int numStreams = 2; + const int chunkSize = N / numStreams; + const size_t bytesPerChunk = chunkSize * sizeof(float); + + // 线程块配置 + const int threadsPerBlock = 256; + const int blocksPerChunk = (chunkSize + threadsPerBlock - 1) / threadsPerBlock; + + // 分配页锁定主机内存 + float *h_A, *h_B, *h_C; + cudaMallocHost(&h_A, N * sizeof(float)); + cudaMallocHost(&h_B, N * sizeof(float)); + cudaMallocHost(&h_C, N * sizeof(float)); + + // 初始化数据 + initData(h_A, N, 1.0f); + initData(h_B, N, 2.0f); + + // 分配设备内存 + float *d_A, *d_B, *d_C; + cudaMalloc(&d_A, N * sizeof(float)); + cudaMalloc(&d_B, N * sizeof(float)); + cudaMalloc(&d_C, N * sizeof(float)); + + // 创建流和事件 + cudaStream_t streams[numStreams]; + cudaEvent_t startEvent, stopEvent; + for (int i = 0; i < numStreams; i++) { + cudaStreamCreate(&streams[i]); + } + cudaEventCreate(&startEvent); + cudaEventCreate(&stopEvent); + + // 记录开始事件 + cudaEventRecord(startEvent, 0); + + // === 方法1: 使用多流实现数据传输与计算的流水线重叠 === + for (int i = 0; i < numStreams; i++) { + int offset = i * chunkSize; + // 异步拷贝 Host -> Device + cudaMemcpyAsync(d_A + offset, h_A + offset, bytesPerChunk, + cudaMemcpyHostToDevice, streams[i]); + cudaMemcpyAsync(d_B + offset, h_B + offset, bytesPerChunk, + cudaMemcpyHostToDevice, streams[i]); + // 启动核函数 + vectorAdd<<>>( + d_A + offset, d_B + offset, d_C + offset, chunkSize); + // 异步拷贝 Device -> Host + cudaMemcpyAsync(h_C + offset, d_C + offset, bytesPerChunk, + cudaMemcpyDeviceToHost, streams[i]); + } + + // 等待所有流完成 + cudaDeviceSynchronize(); + + // 记录结束事件并计算时间 + cudaEventRecord(stopEvent, 0); + cudaEventSynchronize(stopEvent); + float elapsedTimePipeline; + cudaEventElapsedTime(&elapsedTimePipeline, startEvent, stopEvent); + + // === 方法2: 使用单流(默认流)作为对照 === + // 先重置数据 + initData(h_C, N, 0.0f); + cudaMemset(d_C, 0, N * sizeof(float)); + + cudaEvent_t start2, stop2; + cudaEventCreate(&start2); + cudaEventCreate(&stop2); + + cudaEventRecord(start2, 0); + + // 单流:所有操作按顺序执行 + cudaMemcpy(d_A, h_A, N * sizeof(float), cudaMemcpyHostToDevice); + cudaMemcpy(d_B, h_B, N * sizeof(float), cudaMemcpyHostToDevice); + vectorAdd<<<(N + 255) / 256, 256>>>(d_A, d_B, d_C, N); + cudaMemcpy(h_C, d_C, N * sizeof(float), cudaMemcpyDeviceToHost); + + cudaDeviceSynchronize(); + + cudaEventRecord(stop2, 0); + cudaEventSynchronize(stop2); + float elapsedTimeSingle; + cudaEventElapsedTime(&elapsedTimeSingle, start2, stop2); + + // 输出结果 + printf("向量加法流水线示例 (N = %d, 数据量 = %.2f MB)\n", N, (float)(N * 3 * sizeof(float)) / (1024 * 1024)); + printf("===========================================\n"); + printf("双流流水线执行时间: %.3f ms\n", elapsedTimePipeline); + printf("单流顺序执行时间: %.3f ms\n", elapsedTimeSingle); + printf("性能提升: %.2fx\n", elapsedTimeSingle / elapsedTimePipeline); + + // 验证结果 + if (verifyResult(h_C, N, 3.0f)) { + printf("结果验证: PASS\n"); + } else { + printf("结果验证: FAIL\n"); + } + + // 清理资源 + cudaFreeHost(h_A); + cudaFreeHost(h_B); + cudaFreeHost(h_C); + cudaFree(d_A); + cudaFree(d_B); + cudaFree(d_C); + for (int i = 0; i < numStreams; i++) { + cudaStreamDestroy(streams[i]); + } + cudaEventDestroy(startEvent); + cudaEventDestroy(stopEvent); + cudaEventDestroy(start2); + cudaEventDestroy(stop2); + + return 0; +} +``` + +### 7.7.2 编译与运行 + +```bash +nvcc -o pipeline_streams pipeline_streams.cu +./pipeline_streams +``` + +### 7.7.3 预期输出与分析 + +``` +向量加法流水线示例 (N = 4194304, 数据量 = 48.00 MB) +=========================================== +双流流水线执行时间: 15.234 ms +单流顺序执行时间: 24.567 ms +性能提升: 1.61x +结果验证: PASS +``` + +在支持并发数据传输和核函数执行重叠的 GPU 上,多流版本通常能获得 1.5x 到 2.0x 的性能提升。这是因为: + +1. 当 `stream[0]` 在设备上执行核函数时,`stream[1]` 可以同时进行主机到设备的数据传输。 +2. 反之亦然,当 `stream[1]` 执行核函数时,`stream[0]` 的结果可以传回主机。 + +这种"乒乓"式的流水线模式是高效 GPU 编程的核心技术之一。 + +### 7.7.4 使用 Nsight Systems 可视化分析 + +推荐使用 NVIDIA Nsight Systems 来可视化流重叠行为。运行以下命令: + +```bash +nsys profile --stats=true ./pipeline_streams +``` + +Nsight Systems 的时间线视图可以直观地展示哪些操作在时间上重叠,哪些操作被串行化了。这是理解和优化异步并发行为的重要工具。 + +## 7.8 深入理解异步操作 + +### 7.8.1 异步拷贝引擎(DMA Engine) + +现代 NVIDIA GPU 内部包含专用的DMA(Direct Memory Access,直接内存访问)引擎(也称拷贝引擎),它们独立于计算核心(CUDA Cores)运行。这些 DMA 引擎负责执行主机与设备之间的数据传输以及设备内部的数据传输。 + +`asyncEngineCount` 设备属性告诉我们 GPU 有多少个独立的 DMA 拷贝引擎: + +- `asyncEngineCount == 0`:设备不支持异步拷贝与核函数执行的重叠; +- `asyncEngineCount == 1`:设备有一个拷贝引擎,可以执行一个方向(H2D 或 D2H)的异步拷贝与核函数重叠; +- `asyncEngineCount == 2`:设备有两个拷贝引擎,可以同时进行 H2D 和 D2H 拷贝,且都能与核函数执行重叠。 + +需要注意的是,DMA 引擎和计算核心共享 PCIe/NVLink 总线带宽。即使有两个 DMA 引擎,如果总线带宽已经成为瓶颈,同时进行两个方向的传输也不会获得双倍的总带宽。但重叠的好处在于:当 DMA 引擎在传输数据时,计算核心可以同时执行核函数——这就实现了数据传输隐藏(data transfer hiding)。 + +### 7.8.2 深入理解隐式同步 + +隐式同步是多流编程中最容易产生困惑的地方。让我们仔细分析每种会触发隐式同步的操作: + +(1)设备内存分配(cudaMalloc) + +`cudaMalloc` 是同步操作,它会等待所有设备操作完成。这是因为内存分配可能需要重新组织设备内存布局。如果你在多流编程的关键路径中使用 `cudaMalloc`,它会破坏所有的异步并发。 + +解决方案:在程序开始时预分配所有设备内存,避免在流的执行过程中进行设备内存分配。 + +(2)页锁定主机内存分配(cudaMallocHost) + +类似地,`cudaMallocHost` 也是同步的。但它的同步范围更大——它涉及操作系统级别的页面锁定。 + +(3)NULL 流中的任何命令 + +如果你使用 legacy 默认流模式(即 NULL 流),任何发往流 0(NULL 流)的命令都会等待所有其他流中的操作完成,同时其他流也必须等待 NULL 流中的操作完成。这实际上使所有流串行化。 + +```cuda +// 在 legacy 默认流模式下: +kernelA<<>>(...); // 在 stream[0] 中异步执行 +kernelB<<>>(...); // 在 NULL 流中执行——会隐式等待 stream[0] +kernelC<<>>(...); // 必须等待 kernelB 完成 +``` + +解决方案:使用 per-thread 默认流模式或在所有显式创建的非 NULL 流上操作,完全避免使用 NULL 流。 + +(4)设备内存设置(cudaMemset) + +`cudaMemset` 是同步的。如果要异步设置设备内存,请使用 `cudaMemsetAsync`。 + +### 7.8.3 多流编程的最佳实践 + +基于以上分析,总结多流编程的最佳实践: + +1. 预分配内存:在程序初始化阶段分配所有需要的设备内存和页锁定主机内存,避免在性能关键路径上进行内存分配。 + +2. 使用 per-thread 默认流:启用 `--default-stream per-thread` 编译选项,避免 NULL 流的隐式同步。 + +3. 同类操作聚合:将相同类型的操作(如所有的 H2D 拷贝)集中发出,而不是在每个流中交错不同类型的操作。这能最大化并发潜力。 + +4. 按需同步:只在需要的时候同步特定的流(使用cudaStreamSynchronize()),避免不必要的全局同步(cudaDeviceSynchronize())。全局同步会阻塞所有流的操作,严重影响并发性能 "。 + +5. 使用事件进行跨流依赖:当流之间确实需要依赖关系时,使用 `cudaEventRecord()` 和 `cudaStreamWaitEvent()` 来精确控制,而不是依赖隐式同步。 + +6. 监控占用与重叠:使用 Nsight Systems 等工具可视化流的执行时间线,确认操作是否如预期般重叠。 + +### 7.8.4 流的调试技巧 + +异步编程的调试比同步编程困难得多,因为错误可能在操作发起很久之后才出现。以下是一些有用的调试技巧: + +(1)使用 CUDA_LAUNCH_BLOCKING + +设置环境变量 `CUDA_LAUNCH_BLOCKING=1` 会使所有核函数启动变为同步。这有助于确定问题是出在某个特定的核函数中,还是出在异步操作的时序上。 + +```bash +CUDA_LAUNCH_BLOCKING=1 ./my_program +``` + +(2)在关键点插入同步 + +在调试期间,可以在每个流操作后插入 `cudaStreamSynchronize()` 来定位问题。一旦问题定位,可以移除这些同步调用以恢复性能。 + +(3)使用 compute-sanitizer + +`compute-sanitizer` 是` cuda-memcheck `的继任者,它可以更全面检测内存越界访问等错误。它默认在同步模式下运行,因此也能帮助定位异步操作中的问题。 + +```bash +compute-sanitizer ./my_program +``` + +(4)Nsight Systems 时间线分析 + +Nsight Systems 提供了 GPU 和 CPU 操作的详细时间线视图,可以直观地看到: +- 哪些操作实际在并发执行; +- 是否存在意外的串行化(隐式同步); +- CUDA API 调用的 CPU 端开销。 + +### 7.8.5 并发核函数执行深入探讨 + +计算能力 2.x 及以上的设备支持并发核函数执行。但是,并发核函数执行的实际发生取决于多个因素: + +资源约束:如果第一个核函数几乎消耗了 SM 的所有资源(寄存器、共享内存),就没有足够的资源来同时运行第二个核函数。在这种情况下,第二个核函数只能等到第一个核函数完成一些线程块并释放资源后才能开始。 + +上下文限制:CUDA 驱动为每个设备维护一个执行队列。来自不同 CUDA 上下文的核函数不能并发执行——它们按上下文以 FIFO 顺序处理。 + +核函数数量限制:一个设备可以并发执行的核函数的最大数量取决于其计算能力。例如,Kepler 架构最多支持 32 个并发核函数,而更新的架构通常支持更多。 + +纹理和表面内存限制:使用大量纹理引用或纹理对象的核函数可能限制并发性,因为纹理缓存是有限的共享资源。 + +### 7.8.6 如何查询设备的并发能力 + +以下程序可以在运行时查询设备的异步并发能力,帮助你了解自己的 GPU 能做什么样的重叠: + +```cuda +#include +#include + +int main() { + int deviceCount; + cudaGetDeviceCount(&deviceCount); + + for (int i = 0; i < deviceCount; i++) { + cudaDeviceProp prop; + cudaGetDeviceProperties(&prop, i); + + printf("=== Device %d: %s ===\n", i, prop.name); + printf(" Compute Capability: %d.%d\n", prop.major, prop.minor); + + // 检查并发能力 + printf("\n Concurrency Capabilities:\n"); + printf(" Concurrent Kernels: %s\n", + prop.concurrentKernels ? "YES" : "NO"); + printf(" Async Engine Count: %d\n", prop.asyncEngineCount); + + // 解释 asyncEngineCount 的含义 + if (prop.asyncEngineCount >= 2) { + printf(" -> Can overlap H2D copy with D2H copy\n"); + } + if (prop.asyncEngineCount >= 1) { + printf(" -> Can overlap data copy with kernel execution\n"); + } else { + printf(" -> Cannot overlap data copy with kernel execution\n"); + } + + // 检查 deviceOverlap 属性 + // 这是对旧代码的兼容性属性 + printf(" Device Overlap: %s\n", + prop.deviceOverlap ? "YES" : "NO"); + + // 最大并发核函数数(对于支持并发核函数的设备) + if (prop.concurrentKernels) { + printf(" Max Concurrent Kernels: typically > 1\n"); + printf(" (Depends on compute capability, see Table 15 in CUDA Guide)\n"); + } + + // 对应用的建议 + printf("\n Recommendations:\n"); + if (prop.asyncEngineCount >= 1) { + printf(" * Use page-locked memory for overlapped transfers\n"); + printf(" * Use multiple streams to hide data transfer latency\n"); + } + if (prop.concurrentKernels) { + printf(" * Consider launching multiple small kernels concurrently\n"); + printf(" * Be mindful of resource constraints (registers, shared memory)\n"); + } + if (prop.asyncEngineCount >= 2 && prop.concurrentKernels) { + printf(" * Full concurrency: can overlap H2D + D2H + Kernel simultaneously\n"); + } + printf("\n"); + } + + return 0; +} +``` + +将此程序编译运行: + +```bash +nvcc -o device_concurrency device_concurrency.cu +./device_concurrency +``` + +输出示例(RTX 3060): + +``` +=== Device 0: NVIDIA GeForce RTX 3060 === + Compute Capability: 8.6 + + Concurrency Capabilities: + Concurrent Kernels: YES + Async Engine Count: 1 + -> Can overlap data copy with kernel execution + Device Overlap: YES + Max Concurrent Kernels: typically > 1 + (Depends on compute capability, see Table 15 in CUDA Guide) + + Recommendations: + * Use page-locked memory for overlapped transfers + * Use multiple streams to hide data transfer latency + * Consider launching multiple small kernels concurrently + * Be mindful of resource constraints (registers, shared memory) +``` + +## 7.9 同步与异步的选择指南 + +### 7.9.1 何时使用同步调用 + +虽然本章主要强调异步并发的优势,但在某些场景下同步调用仍然是正确且合适的选择: + +适合使用同步调用的场景: + +1. 初始化阶段:程序初始化时的内存分配和初始数据传输,此时正确性比性能更重要。 + +2. 小数据传输(<= 64KB):对于小数据块,异步调用的开销可能超过其带来的并发收益。CUDA 运行时本身也认为小于 64KB 的同步拷贝已经足够快。 + +3. 调试阶段:在调试时,同步模式可以更容易地定位错误发生的位置。 + +4. 确定性需求:某些算法需要在每一步之间严格同步(如迭代算法中的残差检查)。 + +### 7.9.2 何时使用异步调用 + +适合使用异步调用的场景: + +1. 大块数据传输:当数据传输时间显著大于异步启动开销时。 + +2. 重复性工作负载:在训练循环、视频处理等重复执行的任务中,流水线模式能带来持续的性能提升。 + +3. 多流流水线:将工作分解为多个独立的子任务,通过多流并发减少总执行时间。 + +4. 主机和设备的并发:当主机也有计算任务需要在 GPU 运行期间执行时(如数据预处理、日志记录等)。 + +### 7.9.3 性能决策流程图 + +选择同步还是异步的简单决策流程: + +``` +需要主机在设备操作期间继续执行其他工作? + └── 是 → 使用异步调用 + 流 + └── 否 → 数据传输量是否 > 64KB? + └── 是 → 数据是否可以分为独立块? + └── 是 → 使用多流流水线(异步) + └── 否 → 同步或异步均可 + └── 否 → 使用同步调用(开销更低) +``` + +## 7.10 本章小结 + +本章全面介绍了 CUDA 的异步并发执行模型,包括以下核心内容: + +1. 异步并发执行是 CUDA 程序获取高性能的关键机制,涉及主机与设备并发、核函数并发、数据传输与核函数重叠、以及并发数据传输。 + +2. 流(Stream)是管理并发操作的基本单位。同一流内的操作按顺序执行,不同流之间可以并发。流的创建、使用和销毁需要谨慎管理。 + +3. 默认流有 legacy 和 per-thread 两种模式。Legacy 模式(NULL 流)会引发隐式同步,而 per-thread 模式下每个线程有自己的默认流,不会相互干扰。 + +4. 显式同步通过 `cudaDeviceSynchronize`、`cudaStreamSynchronize`、`cudaStreamWaitEvent` 等函数实现;隐式同步由内存分配、设备设置等操作触发,需要特别留意。 + +5. 事件(Event)用于精确计时和跨流同步。`cudaEventElapsedTime` 可以测量 GPU 操作的执行时间,`cudaStreamWaitEvent` 则实现灵活的流间依赖。 + +6. 流优先级允许为不同流设置不同的调度优先级,高优先级的流中的工作会被优先处理。 + +7. 主机回调通过 `cudaLaunchHostFunc` 在流中的特定点插入 CPU 函数,但要避免在回调中进行 CUDA 调用。 + +8. 实现数据传输和核函数执行的重叠需要页锁定(pinned)主机内存、合理的流创建策略,以及注意命令发出的顺序。 + +## 7.11 习题 + +### 习题 7.1:概念理解 + +(a) 解释为什么页锁定(pinned)主机内存对于实现数据传输与核函数执行的重叠是必需的。 + +(b) NULL 流和 per-thread 默认流的根本区别是什么?在什么场景下你会选择使用 per-thread 默认流? + +(c) 列出至少三种会触发隐式同步的 CUDA 操作,并解释为什么它们需要隐式同步。 + +(d) `cudaDeviceSynchronize()` 和 `cudaStreamSynchronize(stream)` 的区别是什么?在什么场景下你会选择使用后者而非前者? + +### 习题 7.2:代码分析 + +(a) 阅读以下代码片段,分析 `stream[0]` 和 `stream[1]` 中的操作是否能够重叠执行。如果能够重叠,请说明哪些操作对会重叠;如果不能,请解释原因。 + +```cuda +cudaMemcpyAsync(d_a, h_a, size, cudaMemcpyHostToDevice, stream[0]); +kernelA<<>>(d_a, d_b); +cudaMemcpyAsync(h_b, d_b, size, cudaMemcpyDeviceToHost, stream[0]); +cudaMemcpyAsync(d_c, h_c, size, cudaMemcpyHostToDevice, stream[1]); +kernelB<<>>(d_c, d_d); +cudaMemcpyAsync(h_d, d_d, size, cudaMemcpyDeviceToHost, stream[1]); +``` + +(b) 如果将上述代码改写为以下形式,重叠情况有何变化? + +```cuda +cudaMemcpyAsync(d_a, h_a, size, cudaMemcpyHostToDevice, stream[0]); +cudaMemcpyAsync(d_c, h_c, size, cudaMemcpyHostToDevice, stream[1]); +kernelA<<>>(d_a, d_b); +kernelB<<>>(d_c, d_d); +cudaMemcpyAsync(h_b, d_b, size, cudaMemcpyDeviceToHost, stream[0]); +cudaMemcpyAsync(h_d, d_d, size, cudaMemcpyDeviceToHost, stream[1]); +``` + +### 习题 7.3:编程实践 + +编写一个 CUDA 程序,实现以下要求: + +(a) 创建 4 个流,将一个大数组分成 4 个数据块,每个流处理一个数据块。每个流先执行主机到设备的数据传输,然后启动核函数(向量加法即可),最后将结果传回主机。 + +(b) 使用事件精确测量整个操作的总执行时间。 + +(c) 对比使用 1 个流和 4 个流的性能差异,分析加速比。 + +(d) 尝试使用流优先级,将第 1 个流的优先级设为最高,观察是否对执行顺序产生影响。 + +(e) 使用 Nsight Systems 分析你的程序,观察时间线视图中各流操作的重叠情况,并截图说明。 + +## 7.12 参考文献 + +1. NVIDIA CUDA C++ Programming Guide (version 11.5.1), Section 3.2.6: Asynchronous Concurrent Execution. Available at: https://docs.nvidia.com/cuda/archive/11.5.1/cuda-c-programming-guide/index.html#asynchronous-concurrent-execution + +2. NVIDIA CUDA C++ Programming Guide (version 13.0), Section 6.2.8: Asynchronous Concurrent Execution. Available at: https://docs.nvidia.com/cuda/cuda-c-programming-guide/ + +3. NVIDIA CUDA Runtime API Reference, Stream Management. Available at: https://docs.nvidia.com/cuda/cuda-runtime-api/ + +4. NVIDIA CUDA Runtime API Reference, Event Management. Available at: https://docs.nvidia.com/cuda/cuda-runtime-api/ + +5. Harris, M. "How to Overlap Data Transfers in CUDA C/C++." NVIDIA Developer Blog, 2012. \ No newline at end of file diff --git "a/outputs/gpu-programming-course/docs/chapter7/\347\254\2547\347\253\240 \346\265\201\343\200\201\344\272\213\344\273\266\344\270\216\345\274\202\346\255\245\345\271\266\345\217\221\346\211\247\350\241\214.md" "b/outputs/gpu-programming-course/docs/chapter7/\347\254\2547\347\253\240 \346\265\201\343\200\201\344\272\213\344\273\266\344\270\216\345\274\202\346\255\245\345\271\266\345\217\221\346\211\247\350\241\214.md" index e88a2f0..fe90c2a 100644 --- "a/outputs/gpu-programming-course/docs/chapter7/\347\254\2547\347\253\240 \346\265\201\343\200\201\344\272\213\344\273\266\344\270\216\345\274\202\346\255\245\345\271\266\345\217\221\346\211\247\350\241\214.md" +++ "b/outputs/gpu-programming-course/docs/chapter7/\347\254\2547\347\253\240 \346\265\201\343\200\201\344\272\213\344\273\266\344\270\216\345\274\202\346\255\245\345\271\266\345\217\221\346\211\247\350\241\214.md" @@ -28,8 +28,8 @@ CUDA 通过异步库函数(asynchronous library functions) 以下设备操作相对于主机是异步的: - 核函数启动(Kernel launches); -- 单个设备内部的内存拷贝; -- 64 KB 或更小的主机到设备内存块拷贝; +- 使用cudaMemcpyAsync的单个设备内部的内存拷贝; +- 从页锁定主机内存到设备的内存拷贝(大小不限); - 以 `Async` 为后缀的内存拷贝函数; - 内存设置函数调用(memory set function calls)。 @@ -42,7 +42,7 @@ CUDA 通过异步库函数(asynchronous library functions) 计算能力 2.x 及以上的某些设备可以同时执行多个核函数。应用程序可以通过检查 `concurrentKernels` 设备属性来查询这一能力,如果设备支持该特性,该属性值为 1。 一个设备能够并发执行的核函数启动的最大数量取决于其计算能力。但需要注意: -- 来自一个 CUDA 上下文的核函数不能与来自另一个 CUDA 上下文的核函数并发执行; +- 仅计算能力 2.x(Fermi 架构)的设备存在限制:来自一个 CUDA 上下文的核函数不能与来自另一个 CUDA 上下文的核函数并发执行;计算能力 3.0(Kepler 架构)及以上的设备通过 Hyper-Q 技术支持不同 CUDA 上下文的核函数并发执行; - 使用大量纹理或大量局部内存的核函数不太可能与其他核函数并发执行。 ### 7.2.3 数据传输与核函数执行的重叠 @@ -121,7 +121,7 @@ for (int i = 0; i < 2; ++i) 有多种方式可以显式地同步流: -- `cudaDeviceSynchronize()`:等待所有主机线程的所有流中的所有先前命令完成。这是最"重"的同步方式,会阻塞主机直到整个 GPU 上的所有工作完成。 +- `cudaDeviceSynchronize()`:等待当前主机线程在当前设备上发起的所有流中的所有先前命令完成。这是最"重"的同步方式,会阻塞主机直到整个 GPU 上的所有工作完成。 - `cudaStreamSynchronize(stream)`:接受一个流作为参数,等待该给定流中的所有先前命令完成。它可以用于将主机与一个特定流同步,同时允许其他流继续在设备上执行。这是更精细的同步方式。 @@ -136,7 +136,7 @@ for (int i = 0; i < 2; ++i) - 页锁定主机内存分配(page-locked host memory allocation); - 设备内存分配(device memory allocation); - 设备内存设置(device memory set); -- 同一设备内存上两个地址之间的内存拷贝; +- 同步的同一设备内存上两个地址之间的内存拷贝; - 发往 NULL 流的任何 CUDA 命令; - L1/共享内存配置的切换。 @@ -537,7 +537,7 @@ kernelC<<>>(...); // 必须等待 kernelB 完成 3. 同类操作聚合:将相同类型的操作(如所有的 H2D 拷贝)集中发出,而不是在每个流中交错不同类型的操作。这能最大化并发潜力。 -4. 批量同步:使用 `cudaDeviceSynchronize()` 一次性同步所有流,而不是逐个流调用 `cudaStreamSynchronize()`,除非有明确的分阶段同步需求。 +4. 按需同步:只在需要的时候同步特定的流(使用cudaStreamSynchronize()),避免不必要的全局同步(cudaDeviceSynchronize())。全局同步会阻塞所有流的操作,严重影响并发性能 "。 5. 使用事件进行跨流依赖:当流之间确实需要依赖关系时,使用 `cudaEventRecord()` 和 `cudaStreamWaitEvent()` 来精确控制,而不是依赖隐式同步。 @@ -559,12 +559,12 @@ CUDA_LAUNCH_BLOCKING=1 ./my_program 在调试期间,可以在每个流操作后插入 `cudaStreamSynchronize()` 来定位问题。一旦问题定位,可以移除这些同步调用以恢复性能。 -(3)使用 cuda-memcheck +(3)使用 compute-sanitizer -`cuda-memcheck` 工具可以检测内存越界访问等错误。它默认在同步模式下运行,因此也能帮助定位异步操作中的问题。 +`compute-sanitizer` 是` cuda-memcheck `的继任者,它可以更全面检测内存越界访问等错误。它默认在同步模式下运行,因此也能帮助定位异步操作中的问题。 ```bash -cuda-memcheck ./my_program +compute-sanitizer ./my_program ``` (4)Nsight Systems 时间线分析 @@ -580,7 +580,7 @@ Nsight Systems 提供了 GPU 和 CPU 操作的详细时间线视图,可以直 资源约束:如果第一个核函数几乎消耗了 SM 的所有资源(寄存器、共享内存),就没有足够的资源来同时运行第二个核函数。在这种情况下,第二个核函数只能等到第一个核函数完成一些线程块并释放资源后才能开始。 -上下文限制:CUDA 驱动为每个设备维护一个执行队列。来自不同 CUDA 上下文的核函数不能并发执行——它们按上下文以 FIFO 顺序处理。 +上下文限制:仅计算能力 2.x(Fermi 架构)的设备为每个设备维护单一执行队列,来自不同 CUDA 上下文的核函数不能并发执行,按上下文以 FIFO 顺序处理;计算能力 3.0(Kepler 架构)及以上的设备通过 Hyper-Q 技术支持最多 32 个独立执行队列,允许来自不同 CUDA 上下文的核函数并发执行。 核函数数量限制:一个设备可以并发执行的核函数的最大数量取决于其计算能力。例如,Kepler 架构最多支持 32 个并发核函数,而更新的架构通常支持更多。 diff --git a/outputs/gpu-programming-course/docs/chapter8/example1.cu b/outputs/gpu-programming-course/docs/chapter8/example1.cu new file mode 100644 index 0000000..bbbc486 --- /dev/null +++ b/outputs/gpu-programming-course/docs/chapter8/example1.cu @@ -0,0 +1,9 @@ +int deviceCount; +cudaGetDeviceCount(&deviceCount); +int device; +for (device = 0; device < deviceCount; ++device) { + cudaDeviceProp deviceProp; + cudaGetDeviceProperties(&deviceProp, device); + printf("Device %d has compute capability %d.%d.\n", + device, deviceProp.major, deviceProp.minor); +} \ No newline at end of file diff --git "a/outputs/gpu-programming-course/docs/chapter8/\347\254\2548\347\253\240 \345\244\232\350\256\276\345\244\207\347\263\273\347\273\237\344\270\216\347\273\237\344\270\200\350\231\232\346\213\237\345\234\260\345\235\200\347\251\272\351\227\264.md" "b/outputs/gpu-programming-course/docs/chapter8/\347\254\2548\347\253\240 \345\244\232\350\256\276\345\244\207\347\263\273\347\273\237\344\270\216\347\273\237\344\270\200\350\231\232\346\213\237\345\234\260\345\235\200\347\251\272\351\227\264.md" index c4b7e03..65eb017 100644 --- "a/outputs/gpu-programming-course/docs/chapter8/\347\254\2548\347\253\240 \345\244\232\350\256\276\345\244\207\347\263\273\347\273\237\344\270\216\347\273\237\344\270\200\350\231\232\346\213\237\345\234\260\345\235\200\347\251\272\351\227\264.md" +++ "b/outputs/gpu-programming-course/docs/chapter8/\347\254\2548\347\253\240 \345\244\232\350\256\276\345\244\207\347\263\273\347\273\237\344\270\216\347\273\237\344\270\200\350\231\232\346\213\237\345\234\260\345\235\200\347\251\272\351\227\264.md" @@ -480,102 +480,98 @@ int main() { prop.totalGlobalMem / (1024.0 * 1024.0 * 1024.0)); } - // 检查设备 0 和 1 之间的 P2P 能力 + // 检查 P2P int canAccess01, canAccess10; CUDA_CHECK(cudaDeviceCanAccessPeer(&canAccess01, 0, 1)); CUDA_CHECK(cudaDeviceCanAccessPeer(&canAccess10, 1, 0)); - printf("\nP2P Access: Device 0 -> Device 1: %s\n", - canAccess01 ? "Supported" : "Not Supported"); - printf("P2P Access: Device 1 -> Device 0: %s\n", - canAccess10 ? "Supported" : "Not Supported"); + printf("\nP2P Access: Device 0 -> 1: %s\n", canAccess01 ? "YES" : "NO"); + printf("P2P Access: Device 1 -> 0: %s\n", canAccess10 ? "YES" : "NO"); - // 测试数据大小:16MB - const size_t dataSize = 16 * 1024 * 1024; // 16 MB - const size_t numFloats = dataSize / sizeof(float); - float *d_data0, *d_data1; - float *h_data; + const size_t dataSize = 16 << 20; + float *d0, *d1, *h_data; - // 分配设备内存 + // 分配内存 CUDA_CHECK(cudaSetDevice(0)); - CUDA_CHECK(cudaMalloc(&d_data0, dataSize)); + CUDA_CHECK(cudaMalloc(&d0, dataSize)); CUDA_CHECK(cudaSetDevice(1)); - CUDA_CHECK(cudaMalloc(&d_data1, dataSize)); - - // 分配页锁定主机内存 + CUDA_CHECK(cudaMalloc(&d1, dataSize)); CUDA_CHECK(cudaMallocHost(&h_data, dataSize)); - // 初始化数据 - for (size_t i = 0; i < numFloats; i++) { - h_data[i] = (float)i; - } + // 初始化 + for (size_t i = 0; i < dataSize / sizeof(float); i++) + h_data[i] = i; - // 将数据拷贝到设备 0 CUDA_CHECK(cudaSetDevice(0)); - CUDA_CHECK(cudaMemcpy(d_data0, h_data, dataSize, cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaMemcpy(d0, h_data, dataSize, cudaMemcpyHostToDevice)); - // 创建事件用于计时 + // ========================== + // ✅ 修复 1:事件必须在设备 0 创建 & 只在设备 0 使用 + // ========================== cudaEvent_t start, stop; + CUDA_CHECK(cudaSetDevice(0)); CUDA_CHECK(cudaEventCreate(&start)); CUDA_CHECK(cudaEventCreate(&stop)); - float elapsedTime; + float ms; + + // ========================== + // 先启用 P2P! + // ========================== + if (canAccess01 && canAccess10) { + CUDA_CHECK(cudaSetDevice(0)); + CUDA_CHECK(cudaDeviceEnablePeerAccess(1, 0)); + CUDA_CHECK(cudaSetDevice(1)); + CUDA_CHECK(cudaDeviceEnablePeerAccess(0, 0)); + } - // === 测试1: P2P 直接拷贝(使用 cudaMemcpyPeer)=== + // ========================== + // 测试 1:P2P 拷贝(正确用法) + // ========================== CUDA_CHECK(cudaSetDevice(0)); - CUDA_CHECK(cudaEventRecord(start, 0)); - CUDA_CHECK(cudaMemcpyPeer(d_data1, 1, d_data0, 0, dataSize)); - CUDA_CHECK(cudaEventRecord(stop, 0)); + CUDA_CHECK(cudaEventRecord(start)); + CUDA_CHECK(cudaMemcpyPeer(d1, 1, d0, 0, dataSize)); + CUDA_CHECK(cudaEventRecord(stop)); CUDA_CHECK(cudaEventSynchronize(stop)); - CUDA_CHECK(cudaEventElapsedTime(&elapsedTime, start, stop)); - printf("\n=== P2P Memory Copy Performance ===\n"); - printf("P2P direct copy (cudaMemcpyPeer): %.3f ms, Bandwidth: %.2f GB/s\n", - elapsedTime, (dataSize / (elapsedTime / 1000.0)) / (1024.0 * 1024.0 * 1024.0)); + CUDA_CHECK(cudaEventElapsedTime(&ms, start, stop)); + printf("\nP2P cudaMemcpyPeer: %.2f ms\n", ms); - // === 测试2: 通过主机中转的拷贝 === + // ========================== + // 测试 2:主机中转 + // ========================== CUDA_CHECK(cudaSetDevice(0)); - CUDA_CHECK(cudaEventRecord(start, 0)); - // 设备 0 -> 主机 - CUDA_CHECK(cudaMemcpy(h_data, d_data0, dataSize, cudaMemcpyDeviceToHost)); - // 主机 -> 设备 1 + CUDA_CHECK(cudaEventRecord(start)); + CUDA_CHECK(cudaMemcpy(h_data, d0, dataSize, cudaMemcpyDeviceToHost)); CUDA_CHECK(cudaSetDevice(1)); - CUDA_CHECK(cudaMemcpy(d_data1, h_data, dataSize, cudaMemcpyHostToDevice)); - CUDA_CHECK(cudaEventRecord(stop, 0)); + CUDA_CHECK(cudaMemcpy(d1, h_data, dataSize, cudaMemcpyHostToDevice)); + CUDA_CHECK(cudaSetDevice(0)); + CUDA_CHECK(cudaEventRecord(stop)); CUDA_CHECK(cudaEventSynchronize(stop)); - CUDA_CHECK(cudaEventElapsedTime(&elapsedTime, start, stop)); - printf("Host-staged copy (D2H + H2D): %.3f ms, Bandwidth: %.2f GB/s\n", - elapsedTime, (dataSize / (elapsedTime / 1000.0)) / (1024.0 * 1024.0 * 1024.0)); + CUDA_CHECK(cudaEventElapsedTime(&ms, start, stop)); + printf("Host staging: %.2f ms\n", ms); - // === 测试3: 尝试启用 P2P 访问后的 UVA 直接访问 === + // ========================== + // 测试 3:UDA cudaMemcpyDefault + // ========================== if (canAccess01 && canAccess10) { CUDA_CHECK(cudaSetDevice(0)); - CUDA_CHECK(cudaDeviceEnablePeerAccess(1, 0)); - CUDA_CHECK(cudaSetDevice(1)); - CUDA_CHECK(cudaDeviceEnablePeerAccess(0, 0)); - - // 使用 cudaMemcpyDefault 进行 P2P 拷贝 - CUDA_CHECK(cudaSetDevice(0)); - CUDA_CHECK(cudaEventRecord(start, 0)); - CUDA_CHECK(cudaMemcpy(d_data1, d_data0, dataSize, cudaMemcpyDefault)); - CUDA_CHECK(cudaEventRecord(stop, 0)); + CUDA_CHECK(cudaEventRecord(start)); + CUDA_CHECK(cudaMemcpy(d1, d0, dataSize, cudaMemcpyDefault)); + CUDA_CHECK(cudaEventRecord(stop)); CUDA_CHECK(cudaEventSynchronize(stop)); - CUDA_CHECK(cudaEventElapsedTime(&elapsedTime, start, stop)); - printf("P2P copy via cudaMemcpyDefault: %.3f ms, Bandwidth: %.2f GB/s\n", - elapsedTime, - (dataSize / (elapsedTime / 1000.0)) / (1024.0 * 1024.0 * 1024.0)); - - printf("\nP2P access enabled successfully. Same pointer can be used on both devices.\n"); + CUDA_CHECK(cudaEventElapsedTime(&ms, start, stop)); + printf("P2P default: %.2f ms\n", ms); } - // 清理资源 - CUDA_CHECK(cudaFreeHost(h_data)); + // 释放 CUDA_CHECK(cudaSetDevice(0)); - CUDA_CHECK(cudaFree(d_data0)); + CUDA_CHECK(cudaFree(d0)); CUDA_CHECK(cudaSetDevice(1)); - CUDA_CHECK(cudaFree(d_data1)); + CUDA_CHECK(cudaFree(d1)); + CUDA_CHECK(cudaFreeHost(h_data)); CUDA_CHECK(cudaEventDestroy(start)); CUDA_CHECK(cudaEventDestroy(stop)); - printf("\nTest completed successfully.\n"); + printf("\n✅ Run success!\n"); return 0; } ``` diff --git "a/outputs/gpu-programming-course/docs/chapter9/\347\254\2549\347\253\240 \347\241\254\344\273\266\345\256\236\347\216\260\342\200\224\342\200\224SIMT\346\236\266\346\236\204\344\270\216Warp\350\260\203\345\272\246.md" "b/outputs/gpu-programming-course/docs/chapter9/\347\254\2549\347\253\240 \347\241\254\344\273\266\345\256\236\347\216\260\342\200\224\342\200\224SIMT\346\236\266\346\236\204\344\270\216Warp\350\260\203\345\272\246.md" index cbd43db..7cd3c98 100644 --- "a/outputs/gpu-programming-course/docs/chapter9/\347\254\2549\347\253\240 \347\241\254\344\273\266\345\256\236\347\216\260\342\200\224\342\200\224SIMT\346\236\266\346\236\204\344\270\216Warp\350\260\203\345\272\246.md" +++ "b/outputs/gpu-programming-course/docs/chapter9/\347\254\2549\347\253\240 \347\241\254\344\273\266\345\256\236\347\216\260\342\200\224\342\200\224SIMT\346\236\266\346\236\204\344\270\216Warp\350\260\203\345\272\246.md" @@ -111,16 +111,16 @@ __global__ void divergentKernel(float* data, int N) { ```cuda // 不安全的 warp 内归约(在 Volta+ 上可能产生错误结果) +// 旧版使用了无掩码的 __shfl_down,依赖 warp 隐式锁步执行 __device__ int warpReduce(int val) { - // 假设 warp 内的线程以锁步方式执行 for (int offset = 16; offset > 0; offset /= 2) { - val += __shfl_down_sync(0xFFFFFFFF, val, offset); + val += __shfl_down(val, offset); // 无掩码,Volta+ 不再可用 } return val; } ``` +在 Volta 及更新架构中,`__shfl_down` 已被弃用,必须改用` __shfl_down_sync(mask, ...)`,并明确指定参与线程的掩码;如果线程在调用前可能因分支而发散,还需使用 `__syncwarp() `先进行同步,以确保掩码内的线程处于相同执行位置。 -在 Volta+ 上,需要使用明确的同步掩码(mask)和 `__syncwarp()` 来确保正确性。 ### 9.3.7 活跃线程与不活跃线程 @@ -174,7 +174,7 @@ occupancy = activeWarpsPerSM / maxWarpsPerSM 占用率受以下因素的限制: -1. 每块线程数(Threads per Block):块大小必须是 warp 大小(32)的倍数,并且不能超过设备的 `maxThreadsPerBlock` 限制。 +1. 每块线程数(Threads per Block):块大小推荐是 warp 大小(32)的倍数,并且不能超过设备的 `maxThreadsPerBlock` 限制。 2. 每线程寄存器数(Registers per Thread):核函数使用的寄存器越多,每个 SM 能驻留的 warp 就越少。可以通过 `--maxrregcount` 编译器标志来限制寄存器使用量。