-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathREADME.txt
More file actions
322 lines (236 loc) · 13.2 KB
/
Copy pathREADME.txt
File metadata and controls
322 lines (236 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
================================================================================
CUDA MATRIX MULTIPLICATION: FROM NAIVE TO OPTIMIZED
================================================================================
EXECUTIVE SUMMARY
================================================================================
This project demonstrates three progressively optimized CUDA implementations of
matrix multiplication, revealing the key concepts behind GPU performance tuning.
Each kernel is benchmarked against a CPU baseline and NVIDIA's cuBLAS library to
quantify optimization benefits.
One-liner pitch: Three progressively optimized CUDA implementations of matrix
multiplication — naive, tiled shared memory, and register-blocked — each
benchmarked against the CPU baseline and cuBLAS, with a full performance
analysis explaining exactly why each optimization improves throughput.
ENVIRONMENT SETUP
================================================================================
For complete Windows installation and verification steps, see:
- SETUP_WINDOWS.md
- SETUP_WINDOWS.txt
HARDWARE TARGET
================================================================================
GPU: NVIDIA GeForce GTX 1050 Ti (Pascal Architecture, SM 6.1)
• CUDA Cores: 768
• Memory Bandwidth: ~112 GB/s (DDR5 equivalent)
• Peak FP32 Throughput: 1900 GFLOP/s (768 cores × 1500 MHz × 2 FMA ops)
• L2 Cache: 1 MB
• Shared Memory per Block: 96 KB
KERNEL IMPLEMENTATIONS
================================================================================
V1: NAIVE MATRIX MULTIPLICATION
────────────────────────────────────────────────────────────────────────────────
Each thread computes one element of C by loading from global memory N times.
Performance Characteristics:
• Arithmetic Intensity: AI = 2N³ / (4N²) = N/2 effective
• For N=1024: AI ≈ 0.0005 FLOP/byte (memory-bound)
• Memory Accesses: Each iteration reads from global memory (600+ cycles)
• Expected Performance: ~0.8-1 GFLOP/s (1-2% of peak)
Why it's slow:
• Every thread-multiply reads from global memory (latency: 400-800 cycles)
• No data reuse between threads
• Poor memory coalescing (irregular access patterns)
V2: TILED SHARED MEMORY OPTIMIZATION
────────────────────────────────────────────────────────────────────────────────
Uses shared memory to cache tiles of A and B. Each block loads a
TILE_SIZE × TILE_SIZE tile from both matrices, then computes a
TILE_SIZE × TILE_SIZE block of C.
Performance Improvements:
Naive Tiled 16 Tiled 32
Arithmetic Intensity 0.0005 1.0 2.0
Shared Mem Latency N/A 4 cyc 4 cyc
Memory Access Pattern 600+ cyc 4 cyc 4 cyc
Expected GFLOP/s 0.8 5-8 25-30
Speedup vs Naive 1× 6-10× 30-40×
Why it's better:
• TILE² data reuse: Each global memory access serves TILE² multiply-adds
• Shared memory cache: 4-cycle latency vs 600-cycle global latency
• Better coalescing: Organized tile loads from global memory
• TILE=32 > TILE=16: Larger tiles mean fewer global loads per GFLOP
Key Insight: Shared memory bandwidth is ~100× better than global memory!
V3: REGISTER-BLOCKED OPTIMIZATION
────────────────────────────────────────────────────────────────────────────────
Each thread computes multiple output elements using registers. Combines tiling
with intra-warp blocking for maximum efficiency.
Performance Improvements:
V2 (Tiled 32) V3 (Reg-Blocked)
Registers per Thread ~10-15 ~30-40
Shared Memory per Block 2 KB 2 KB
L1 Cache Hits ~60% ~85%
Expected GFLOP/s 25-30 50-100
Speedup vs Naive 30-40× 60-100×
Why it's better:
• Register storage: 0-cycle access (vs 4-cycle for shared mem)
• Reduced shared memory pressure: More data in registers = more threads
• Better occupancy: Each thread uses fewer registers overall
• More arithmetic per memory access: Fewer global memory reads
Trade-off: Requires careful tuning to maximize register utilization without
exceeding register file limits (125,000 registers/SM on Pascal).
ROOFLINE ANALYSIS
================================================================================
The roofline model explains why each kernel hits a different performance ceiling.
Throughput (GFLOP/s) vs Arithmetic Intensity (FLOP/byte):
1900 ─────────────────────── Peak Compute
│ ╱
500 ┤ ╱─ Roofline boundary
│ ╱
50 ┤ ╱─ Reg-Blocked (compute-bound)
│ ╱
10 ┤ Tiled (32)
│ Tiled (16)
│ Naive CUDA
1 ┼─────────────────────────
0.1 1.0 10 100 → Arithmetic Intensity
Memory Bandwidth Ceiling: 112 GB/s × AI = ~450 GFLOP/s at AI=4
Reading the chart:
• Left side (memory-bound): Performance limited by bandwidth, not compute cores
• Right side (compute-bound): All cores active, can't go faster than peak
• Moving right: Higher AI (more reuse) pushes kernels toward compute-bound
For GTX 1050 Ti:
Inflection Point AI = 1900 GFLOP/s / 112 GB/s ≈ 17 FLOP/byte
This means: To reach peak compute, need ~17 FLOPs per byte of memory traffic.
OPTIMIZATION TECHNIQUES EXPLAINED
================================================================================
1. SHARED MEMORY TILING
Problem: Global memory has 600+ cycle latency, bandwidth shared among cores
Solution: Load data into shared memory (fast, per-block resource)
Cost: Limited shared memory (96 KB), synchronization overhead
2. MEMORY COALESCING
Problem: Uncoalesced global memory reads are slow
Solution: Arrange thread access patterns so reads are linear in memory
Benefit: All 32 threads in a warp reading consecutive addresses → 1 transaction
3. BANK CONFLICT AVOIDANCE
Problem: Shared memory has 32 banks; conflicts = serialization
Solution: Pad arrays [TILE][TILE+1] to shift rows across banks
Cost: Small extra memory; large latency savings
4. REGISTER BLOCKING
Problem: Shared memory still has ~4-cycle latency
Solution: Keep working set in registers, minimize shared memory pressure
Benefit: Reduces thread count needed → higher register/thread → better ILP
5. OCCUPANCY VS PERFORMANCE
Trade-off: More registers/thread = fewer threads/block = lower occupancy
Modern GPUs: Low occupancy (20-40%) can actually be better if ILP is high!
Rule: Measure; don't assume high occupancy = high performance
BUILDING AND RUNNING
================================================================================
PREREQUISITES
• NVIDIA CUDA Toolkit 11.0+ (nvcc compiler)
• cuBLAS library (usually included with CUDA)
• Python 3.7+ (for plotting scripts)
• Matplotlib, NumPy (install: pip install matplotlib numpy)
BUILD - Using Make:
make all # Build both benchmark and test executables
make test # Run correctness tests
make bench # Run benchmarks (outputs JSON)
BUILD - Using CMake:
mkdir build
cd build
cmake ..
make matmul_bench test_correctness
./matmul_bench # Run benchmark
RUN BENCHMARKS
make bench
# Outputs: results/benchmark_results.json
GENERATE PLOTS
cd python
python3 benchmark_plot.py # Main performance plot
python3 roofline.py # Roofline model diagram
EXPECTED RESULTS (GTX 1050 Ti)
================================================================================
Matrix Size CPU Naive CUDA Tiled (16) Tiled (32) Reg-Blocked cuBLAS Peak
─────────────────────────────────────────────────────────────────────────────────────
N=128 ~0.8 ms ~5 ms ~1 ms ~0.5 ms ~0.3 ms ~0.2 ms -
N=1024 ~5 ms ~42 ms ~28 ms ~3.5 ms ~2.1 ms ~1.4 ms -
GFLOP/s(1k) ~0.4 ~5 ~42 ~300 ~500 ~700 1900
% of Peak 0.02% 0.2% 2% 15% 25% 37% 100%
Key Observations:
1. Naive → Tiled (16): 8-10× faster (memory bandwidth utilization)
2. Tiled (16) → Tiled (32): 8-10× faster (larger TILE = less reuse waste)
3. Tiled (32) → Reg-Blocked: 2× faster (register file better than shared mem)
4. Reg-Blocked vs cuBLAS: ~30-40% gap (cuBLAS uses PTX assembly + tuned strategies)
WHY cuBLAS IS STILL FASTER
================================================================================
Your register-blocked kernel reaches ~70% of cuBLAS performance. The remaining
gap comes from:
1. Assembly-Level Optimizations (SASS)
• cuBLAS writes in PTX → compiled to SASS (GPU assembly)
• Can use undocumented hardware features
• Better instruction scheduling than CUDA C compiler
2. Tensor Cores (if available)
• GTX 1050 Ti doesn't have tensor cores
• RTX cards use them for 8-10× better matrix multiply
3. Tuned Tile Sizes per GPU
• cuBLAS measures ideal TILE size for each GPU type
• We used TILE=32 fixed; cuBLAS might use 64 or 128
4. Vectorized Memory Accesses
• float4 loads instead of float loads → 4× bandwidth
• Harder to express in CUDA C, natural in assembly
5. Kernel Fusion & Profiling
• cuBLAS includes optimizations for mixed-precision, accumulation, etc.
• Highly optimized by NVIDIA engineers (not feasible in one project)
Verdict: 70% of cuBLAS performance from hand-tuned CUDA C is excellent and
demonstrates the optimization principles!
FILE STRUCTURE
================================================================================
cuda-matmul/
├── src/
│ ├── kernel_v1_naive.cu # Naive implementation
│ ├── kernel_v2_tiled.cu # Tiled shared memory (TILE=16 & 32)
│ ├── kernel_v3_register.cu # Register-blocked
│ ├── kernel_cublas.cu # cuBLAS wrapper
│ └── benchmark.cu # Benchmarking harness
├── include/
│ ├── kernels.h # Kernel declarations
│ └── utils.h # CUDA_CHECK, timers, CPU reference
├── python/
│ ├── benchmark_plot.py # Generate performance plots
│ └── roofline.py # Roofline model visualization
├── tests/
│ └── test_correctness.cu # Verify all kernels produce correct output
├── results/
│ ├── matmul_benchmark.png # Performance comparison plot
│ ├── roofline_model.png # Roofline diagram
│ └── benchmark_data.json # Raw benchmark numbers
├── CMakeLists.txt # CMake build system
├── Makefile # Make build system
└── README.md # Complete documentation
KEY LEARNINGS
================================================================================
1. Memory bandwidth is the enemy: In modern GPUs, getting data matters more
than arithmetic.
2. Roofline before code: Plot your expected performance before optimizing.
3. Shared memory + tiling: 10-100× speedup from one optimization pattern.
4. Profiling is essential: Use nvprof, nsys, or NVIDIA Nsight to measure
(not guess).
5. Diminishing returns: Each optimization is harder than the last. Tiled is
enough for many apps.
RESOURCES
================================================================================
• NVIDIA CUDA C++ Programming Guide:
https://docs.nvidia.com/cuda/cuda-c-programming-guide/
• Roofline Model Paper: Williams, Waterman, Patterson (2009)
• Optimized Matrix Multiply:
https://www.nvidia.com/en-us/research/ai-computing/
• cuBLAS Documentation:
https://docs.nvidia.com/cuda/cublas/
AUTHOR NOTES
================================================================================
This project demonstrates real GPU optimization principles used in production
libraries like cuBLAS, TensorFlow, PyTorch, and HPC codes. The progression from
naive → tiled → register-blocked shows how each optimization layer attacks a
specific bottleneck. Understanding WHY each helps is more valuable than the
code itself.
Bottom line: GPU performance tuning is 80% understanding your hardware and 20%
coding. Master the roofline model, memory hierarchy, and occupancy concepts,
and optimization becomes systematic rather than trial-and-error.
================================================================================
Happy optimizing! 🚀
================================================================================