Spaces:
Sleeping
Sleeping
File size: 23,989 Bytes
88a1dd2 | 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 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 | # H100 Hopper Architecture Deep Dive
## Overview
The NVIDIA H100 GPU, based on the Hopper architecture (sm_90), is the current flagship for AI training and inference. This guide provides a comprehensive deep dive into optimizing CUDA kernels for H100 hardware, with specific focus on patterns used in HuggingFace Diffusers and Transformers models.
## H100 Hardware Specifications
| Specification | Value |
|---|---|
| Architecture | Hopper (sm_90 / sm_90a) |
| Streaming Multiprocessors (SMs) | 132 |
| HBM3 Bandwidth | 3.35 TB/s |
| Shared Memory per SM | 228 KB (max configurable) |
| Typical Shared Memory Config | 192 KB shared + 36 KB L1 |
| L2 Cache | 50 MB |
| FP32 CUDA Cores per SM | 128 |
| Total FP32 CUDA Cores | 16,896 |
| Tensor Cores (4th gen) per SM | 4 |
| Total Tensor Cores | 528 |
| Memory | 80 GB HBM3 |
| Memory Bus Width | 5120-bit |
| TDP | 700W (SXM5) |
| Max Threads per SM | 2048 |
| Max Threads per Block | 1024 |
| Max Warps per SM | 64 |
| Warp Size | 32 |
| Register File per SM | 256 KB |
| Max Registers per Thread | 255 |
## Memory Hierarchy
Understanding the H100 memory hierarchy is essential for writing high-performance kernels.
```
βββββββββββββββββββββββββββββββββββββββββββββββ
β HBM3 β
β 80 GB @ 3.35 TB/s β
β (~400 cycles latency) β
βββββββββββββββββββββββββββββββββββββββββββββββ€
β L2 Cache β
β 50 MB @ ~12 TB/s β
β (~200 cycles latency) β
βββββββββββββββββββββββββββββββββββββββββββββββ€
β L1 Cache / Shared Memory (per SM) β
β 228 KB total (configurable split) β
β Shared Memory: ~19 TB/s effective β
β (~20-30 cycles latency) β
βββββββββββββββββββββββββββββββββββββββββββββββ€
β Register File (per SM) β
β 256 KB @ ~80 TB/s β
β (~1 cycle latency) β
βββββββββββββββββββββββββββββββββββββββββββββββ
```
### Shared Memory / L1 Cache Configuration
The H100 allows flexible partitioning of the 228 KB per-SM memory between shared memory and L1 cache:
| Shared Memory | L1 Cache | Best For |
|---|---|---|
| 0 KB | 228 KB | Pure streaming workloads |
| 28 KB | 200 KB | Simple kernels with small working sets |
| 100 KB | 128 KB | Moderate data reuse |
| 164 KB | 64 KB | Large tile-based algorithms |
| 192 KB | 36 KB | Maximum shared memory (typical for deep learning) |
| 228 KB | 0 KB | Maximum shared memory (extreme cases) |
```cpp
// Request specific shared memory size
cudaFuncSetAttribute(
kernel_fn,
cudaFuncAttributeMaxDynamicSharedMemorySize,
192 * 1024 // 192 KB -- recommended for most DL kernels
);
// Set preferred shared memory carveout
cudaFuncSetAttribute(
kernel_fn,
cudaFuncAttributePreferredSharedMemoryCarveout,
cudaSharedmemCarveoutMaxShared
);
```
## Vectorized Memory Access
Vectorized loads and stores are critical for saturating the H100's 3.35 TB/s bandwidth. The memory bus is 5120 bits wide, so maximizing bytes per transaction is essential.
### BF16 Vectorized Access (bf16x2)
```cpp
#include <cuda_bf16.h>
// Load 2 BF16 values as a single 32-bit operation
__device__ __forceinline__ __nv_bfloat162 load_bf16x2(
const __nv_bfloat16* ptr,
int idx
) {
return reinterpret_cast<const __nv_bfloat162*>(ptr)[idx / 2];
}
// Store 2 BF16 values as a single 32-bit operation
__device__ __forceinline__ void store_bf16x2(
__nv_bfloat16* ptr,
int idx,
__nv_bfloat162 val
) {
reinterpret_cast<__nv_bfloat162*>(ptr)[idx / 2] = val;
}
// Arithmetic on bf16x2 (operates on both elements simultaneously)
__device__ __forceinline__ __nv_bfloat162 bf16x2_add(
__nv_bfloat162 a,
__nv_bfloat162 b
) {
return __hadd2(a, b);
}
__device__ __forceinline__ __nv_bfloat162 bf16x2_mul(
__nv_bfloat162 a,
__nv_bfloat162 b
) {
return __hmul2(a, b);
}
```
### FP16 Vectorized Access (half2)
```cpp
#include <cuda_fp16.h>
// Load 2 FP16 values as a single 32-bit operation
__device__ __forceinline__ half2 load_half2(
const half* ptr,
int idx
) {
return reinterpret_cast<const half2*>(ptr)[idx / 2];
}
// half2 fused multiply-add
__device__ __forceinline__ half2 half2_fma(
half2 a, half2 b, half2 c
) {
return __hfma2(a, b, c);
}
```
### 128-bit Vectorized Access (float4)
```cpp
// Load 16 bytes at once (4 floats, 8 BF16, or 8 FP16 values)
__device__ __forceinline__ float4 load_128bit(
const void* ptr,
int idx // index in units of float4
) {
return reinterpret_cast<const float4*>(ptr)[idx];
}
// Example: Process 8 BF16 values from a single 128-bit load
__device__ void process_bf16_vectorized(
const __nv_bfloat16* input,
__nv_bfloat16* output,
int n
) {
int tid = threadIdx.x + blockIdx.x * blockDim.x;
// Each thread processes 8 BF16 values (128 bits)
int vec_idx = tid;
int elem_idx = vec_idx * 8;
if (elem_idx + 7 < n) {
float4 packed = reinterpret_cast<const float4*>(input)[vec_idx];
__nv_bfloat162* pairs = reinterpret_cast<__nv_bfloat162*>(&packed);
float4 result;
__nv_bfloat162* out_pairs = reinterpret_cast<__nv_bfloat162*>(&result);
#pragma unroll
for (int i = 0; i < 4; i++) {
float lo = __bfloat162float(__low2bfloat16(pairs[i]));
float hi = __bfloat162float(__high2bfloat16(pairs[i]));
// Process (example: ReLU)
lo = fmaxf(lo, 0.0f);
hi = fmaxf(hi, 0.0f);
out_pairs[i] = __halves2bfloat162(
__float2bfloat16(lo),
__float2bfloat16(hi)
);
}
reinterpret_cast<float4*>(output)[vec_idx] = result;
}
}
```
### Memory Coalescing Rules
For optimal memory throughput on H100:
| Access Pattern | Throughput | Recommendation |
|---|---|---|
| Coalesced 128-byte | 100% | Ideal -- 32 threads each load 4 bytes |
| Coalesced 64-byte | ~90% | Acceptable for half/bf16 |
| Strided (stride=2) | ~50% | Avoid or restructure data layout |
| Random | ~10-25% | Use shared memory staging |
```cpp
// GOOD: Coalesced access -- consecutive threads access consecutive memory
int idx = blockIdx.x * blockDim.x + threadIdx.x;
float val = input[idx]; // Thread 0 reads [0], thread 1 reads [1], etc.
// BAD: Strided access -- threads skip elements
int idx = threadIdx.x * stride; // Non-coalesced if stride > 1
float val = input[idx];
// FIX: Use shared memory to restructure access
__shared__ float smem[BLOCK_SIZE * stride];
// Load coalesced into shared memory first, then access with stride from smem
```
## Warp Shuffles
Warp shuffle operations allow threads within a warp to exchange data without shared memory, providing much lower latency.
### Basic Warp Reduction
```cpp
// Sum reduction within a warp (32 threads)
__device__ __forceinline__ float warp_reduce_sum(float val) {
#pragma unroll
for (int offset = 16; offset > 0; offset >>= 1) {
val += __shfl_xor_sync(0xffffffff, val, offset);
}
return val;
}
// Max reduction within a warp
__device__ __forceinline__ float warp_reduce_max(float val) {
#pragma unroll
for (int offset = 16; offset > 0; offset >>= 1) {
val = fmaxf(val, __shfl_xor_sync(0xffffffff, val, offset));
}
return val;
}
```
### Block-Level Reduction Using Warp Shuffles
```cpp
template<int BLOCK_SIZE>
__device__ float block_reduce_sum(float val) {
constexpr int NUM_WARPS = BLOCK_SIZE / 32;
__shared__ float warp_sums[NUM_WARPS];
int warp_id = threadIdx.x / 32;
int lane_id = threadIdx.x % 32;
// Step 1: Reduce within each warp
val = warp_reduce_sum(val);
// Step 2: First thread of each warp writes to shared memory
if (lane_id == 0) {
warp_sums[warp_id] = val;
}
__syncthreads();
// Step 3: First warp reduces across warps
if (warp_id == 0) {
val = (lane_id < NUM_WARPS) ? warp_sums[lane_id] : 0.0f;
val = warp_reduce_sum(val);
}
return val; // Only thread 0 has the final result
}
```
### Warp Shuffle for Data Exchange
```cpp
// Broadcast a value from one thread to all threads in the warp
__device__ __forceinline__ float warp_broadcast(float val, int src_lane) {
return __shfl_sync(0xffffffff, val, src_lane);
}
// Shift values down within a warp (useful for prefix sums)
__device__ __forceinline__ float warp_shift_down(float val, int delta) {
return __shfl_down_sync(0xffffffff, val, delta);
}
// Shift values up within a warp
__device__ __forceinline__ float warp_shift_up(float val, int delta) {
return __shfl_up_sync(0xffffffff, val, delta);
}
```
## Register Optimization
The H100 has 256 KB of registers per SM (65,536 32-bit registers). Register pressure directly impacts occupancy.
### Register Usage Guidelines
| Registers/Thread | Max Threads/SM | Occupancy | Recommendation |
|---|---|---|---|
| 32 | 2048 | 100% | Ideal for simple kernels |
| 64 | 1024 | 50% | Good for moderate complexity |
| 128 | 512 | 25% | Acceptable for compute-bound |
| 255 | 256 | 12.5% | Only for very compute-heavy |
### Controlling Register Usage
```cpp
// Limit register usage per thread
// This increases occupancy at the cost of register spilling
__global__ __launch_bounds__(256, 4) // max 256 threads/block, min 4 blocks/SM
void my_kernel(...) {
// Kernel code
}
// Or use compiler flag:
// --maxrregcount=128
```
### Tips for Reducing Register Pressure
```cpp
// 1. Use local scope to limit variable lifetime
{
float temp = compute_something();
use(temp);
} // temp can be reused by compiler after this scope
// 2. Prefer recomputation over storing intermediate results
// BAD: wastes a register
float x_squared = x * x;
float x_cubed = x_squared * x;
float result = x_cubed + x_squared + x;
// GOOD: recompute instead
float result = x * x * x + x * x + x;
// 3. Use shared memory for values needed across synchronization points
__shared__ float shared_val;
// Instead of: float local_val = compute(); __syncthreads(); use(local_val);
```
## Occupancy Tuning for 132 SMs
The H100 has 132 SMs. Grid and block sizes must be tuned to maximize utilization.
### Grid Sizing
```cpp
// Query SM count at runtime (portable across GPU models)
int num_sms;
cudaDeviceGetAttribute(&num_sms, cudaDevAttrMultiProcessorCount, 0);
// H100: num_sms = 132
// Minimum grid size for full SM utilization
int min_grid = num_sms; // 132
// Recommended: 2-4 blocks per SM for latency hiding
int grid_2x = num_sms * 2; // 264 blocks
int grid_4x = num_sms * 4; // 528 blocks
// For occupancy-limited kernels, use cudaOccupancyMaxActiveBlocksPerMultiprocessor
int max_blocks_per_sm;
cudaOccupancyMaxActiveBlocksPerMultiprocessor(
&max_blocks_per_sm, kernel_fn, block_size, shared_mem_bytes
);
int optimal_grid = num_sms * max_blocks_per_sm;
```
### Block Size Selection
```cpp
// Auto-tune block size using CUDA occupancy API
int min_grid_size, optimal_block_size;
cudaOccupancyMaxPotentialBlockSize(
&min_grid_size,
&optimal_block_size,
kernel_fn,
shared_mem_bytes,
0 // max block size (0 = no limit)
);
printf("Optimal block size: %d\n", optimal_block_size);
printf("Minimum grid size: %d\n", min_grid_size);
```
### Occupancy Calculator
```cpp
void analyze_occupancy(void* kernel, int block_size, size_t shared_mem) {
cudaFuncAttributes attr;
cudaFuncGetAttributes(&attr, kernel);
printf("Registers per thread: %d\n", attr.numRegs);
printf("Shared memory (static): %zu bytes\n", attr.sharedSizeBytes);
printf("Max threads per block: %d\n", attr.maxThreadsPerBlock);
int max_active_blocks;
cudaOccupancyMaxActiveBlocksPerMultiprocessor(
&max_active_blocks, kernel, block_size, shared_mem
);
float occupancy = (float)(max_active_blocks * block_size) / 2048.0f;
printf("Active blocks/SM: %d\n", max_active_blocks);
printf("Occupancy: %.1f%%\n", occupancy * 100.0f);
printf("Active warps/SM: %d / 64\n", max_active_blocks * block_size / 32);
}
```
## Precision: BF16 vs FP16
### Format Comparison
| Property | BF16 | FP16 |
|---|---|---|
| Total bits | 16 | 16 |
| Sign bits | 1 | 1 |
| Exponent bits | 8 | 5 |
| Mantissa bits | 7 | 10 |
| Dynamic range | ~1e-38 to ~3e38 | ~6e-5 to 65504 |
| Precision | ~3 decimal digits | ~3.3 decimal digits |
| Overflow risk | Very low | Moderate |
| H100 Tensor Core | Yes | Yes |
### When to Use Each
```cpp
// BF16: Preferred for most deep learning workloads
// - Wider dynamic range avoids overflow in attention scores
// - Safe for gradient accumulation
// - Native support on H100
__nv_bfloat16 bf16_val = __float2bfloat16(1.0f);
// FP16: Use when precision matters more than range
// - Image pixel values (0-1 range)
// - Already-normalized values
// - When exact reproducibility with FP16 models is needed
half fp16_val = __float2half(1.0f);
```
### Type Conversion Helpers
```cpp
#include <cuda_bf16.h>
#include <cuda_fp16.h>
// BF16 to float
__device__ __forceinline__ float bf16_to_float(__nv_bfloat16 val) {
return __bfloat162float(val);
}
// Float to BF16
__device__ __forceinline__ __nv_bfloat16 float_to_bf16(float val) {
return __float2bfloat16(val);
}
// FP16 to float
__device__ __forceinline__ float fp16_to_float(half val) {
return __half2float(val);
}
// Float to FP16
__device__ __forceinline__ half float_to_fp16(float val) {
return __float2half(val);
}
// BF16 to FP16 (via float intermediate)
__device__ __forceinline__ half bf16_to_fp16(__nv_bfloat16 val) {
return __float2half(__bfloat162float(val));
}
// FP16 to BF16 (via float intermediate)
__device__ __forceinline__ __nv_bfloat16 fp16_to_bf16(half val) {
return __float2bfloat16(__half2float(val));
}
```
## Online Softmax
The online softmax algorithm computes softmax in a single pass, which is critical for memory-efficient attention implementations.
### Algorithm
The standard softmax requires two passes:
1. Compute max over all elements
2. Compute sum of exp(x - max) and normalize
The online algorithm combines these into a single pass:
```cpp
// Online softmax: single-pass algorithm
// Maintains running max and running sum, correcting as new max is found
struct OnlineSoftmaxState {
float max_val;
float sum_exp;
};
__device__ __forceinline__ OnlineSoftmaxState online_softmax_init() {
return {-INFINITY, 0.0f};
}
__device__ __forceinline__ OnlineSoftmaxState online_softmax_update(
OnlineSoftmaxState state,
float new_val
) {
float new_max = fmaxf(state.max_val, new_val);
float correction = expf(state.max_val - new_max);
float new_sum = state.sum_exp * correction + expf(new_val - new_max);
return {new_max, new_sum};
}
__device__ __forceinline__ OnlineSoftmaxState online_softmax_merge(
OnlineSoftmaxState a,
OnlineSoftmaxState b
) {
float new_max = fmaxf(a.max_val, b.max_val);
float new_sum = a.sum_exp * expf(a.max_val - new_max)
+ b.sum_exp * expf(b.max_val - new_max);
return {new_max, new_sum};
}
```
### Warp-Level Online Softmax
```cpp
__device__ OnlineSoftmaxState warp_online_softmax_reduce(
OnlineSoftmaxState state
) {
#pragma unroll
for (int offset = 16; offset > 0; offset >>= 1) {
OnlineSoftmaxState other;
other.max_val = __shfl_xor_sync(0xffffffff, state.max_val, offset);
other.sum_exp = __shfl_xor_sync(0xffffffff, state.sum_exp, offset);
state = online_softmax_merge(state, other);
}
return state;
}
```
### Full Softmax Kernel Using Online Algorithm
```cpp
template<int BLOCK_SIZE>
__global__ void online_softmax_kernel(
const __nv_bfloat16* __restrict__ input,
__nv_bfloat16* __restrict__ output,
const int seq_len
) {
const int row = blockIdx.x;
const int tid = threadIdx.x;
const __nv_bfloat16* row_in = input + row * seq_len;
__nv_bfloat16* row_out = output + row * seq_len;
// Phase 1: Online reduction to find max and sum
OnlineSoftmaxState state = online_softmax_init();
for (int i = tid; i < seq_len; i += BLOCK_SIZE) {
float val = __bfloat162float(row_in[i]);
state = online_softmax_update(state, val);
}
// Warp reduction
state = warp_online_softmax_reduce(state);
// Block reduction
__shared__ OnlineSoftmaxState warp_states[BLOCK_SIZE / 32];
int warp_id = tid / 32;
int lane_id = tid % 32;
if (lane_id == 0) warp_states[warp_id] = state;
__syncthreads();
if (warp_id == 0) {
state = (lane_id < BLOCK_SIZE / 32)
? warp_states[lane_id]
: online_softmax_init();
state = warp_online_softmax_reduce(state);
}
__shared__ float s_max, s_sum_inv;
if (tid == 0) {
s_max = state.max_val;
s_sum_inv = 1.0f / state.sum_exp;
}
__syncthreads();
// Phase 2: Apply softmax
for (int i = tid; i < seq_len; i += BLOCK_SIZE) {
float val = __bfloat162float(row_in[i]);
float result = expf(val - s_max) * s_sum_inv;
row_out[i] = __float2bfloat16(result);
}
}
```
## Diffusers-Specific Optimizations
### Fused RMSNorm + Linear
A common pattern in transformer blocks is RMSNorm followed by a linear layer. Fusing these avoids an extra memory round-trip:
```cpp
// Instead of:
// normalized = rmsnorm(x) // Read x, write normalized
// output = linear(normalized) // Read normalized, write output
//
// Fused version: reads x once, applies both operations
template<int BLOCK_SIZE, int HIDDEN_SIZE>
__global__ void fused_rmsnorm_linear_kernel(
const __nv_bfloat16* __restrict__ input, // [batch, seq, hidden]
const __nv_bfloat16* __restrict__ norm_weight, // [hidden]
const __nv_bfloat16* __restrict__ linear_weight, // [out_features, hidden]
const __nv_bfloat16* __restrict__ linear_bias, // [out_features] or nullptr
__nv_bfloat16* __restrict__ output, // [batch, seq, out_features]
const int out_features,
const float epsilon
) {
// Implementation would combine RMSNorm and matrix-vector multiply
// This is most beneficial when out_features is small (e.g., QKV projection)
// ...
}
```
### Fused GEGLU
GEGLU splits the input tensor, applies GELU to one half, and multiplies:
```cpp
__global__ void fused_geglu_kernel(
const __nv_bfloat16* __restrict__ input,
__nv_bfloat16* __restrict__ output,
const int batch_size,
const int seq_len,
const int hidden_size // Full hidden size (2x output)
) {
const int half_hidden = hidden_size / 2;
const int total_elements = batch_size * seq_len * half_hidden;
const int idx = blockIdx.x * blockDim.x + threadIdx.x;
// Process 8 elements per thread for vectorized access
const int vec_idx = idx * 8;
if (vec_idx + 7 < total_elements) {
int linear_idx = vec_idx;
int row = linear_idx / half_hidden;
int col = linear_idx % half_hidden;
// Load x and gate from interleaved or split layout
// (depends on model -- LTX-Video splits along last dim)
float4 x_packed = reinterpret_cast<const float4*>(
input + row * hidden_size + col
)[0];
float4 g_packed = reinterpret_cast<const float4*>(
input + row * hidden_size + half_hidden + col
)[0];
__nv_bfloat162* x_pairs = reinterpret_cast<__nv_bfloat162*>(&x_packed);
__nv_bfloat162* g_pairs = reinterpret_cast<__nv_bfloat162*>(&g_packed);
float4 result;
__nv_bfloat162* out_pairs = reinterpret_cast<__nv_bfloat162*>(&result);
#pragma unroll
for (int i = 0; i < 4; i++) {
float x0 = __bfloat162float(__low2bfloat16(x_pairs[i]));
float x1 = __bfloat162float(__high2bfloat16(x_pairs[i]));
float g0 = __bfloat162float(__low2bfloat16(g_pairs[i]));
float g1 = __bfloat162float(__high2bfloat16(g_pairs[i]));
// Apply GELU to gate
g0 = g0 * 0.5f * (1.0f + tanhf(0.7978845608f * (g0 + 0.044715f * g0 * g0 * g0)));
g1 = g1 * 0.5f * (1.0f + tanhf(0.7978845608f * (g1 + 0.044715f * g1 * g1 * g1)));
out_pairs[i] = __halves2bfloat162(
__float2bfloat16(x0 * g0),
__float2bfloat16(x1 * g1)
);
}
reinterpret_cast<float4*>(output + row * half_hidden + col)[0] = result;
}
}
```
## Profiling with nsys and ncu
### nsys (Nsight Systems) -- Timeline Profiling
```bash
# Basic timeline profile
nsys profile -o h100_trace python inference.py
# With detailed CUDA and NVTX tracing
nsys profile \
--trace=cuda,cudnn,cublas,nvtx,osrt \
--cuda-memory-usage=true \
--stats=true \
-o h100_detailed \
python inference.py
# Focus on specific time range
nsys profile \
--delay=5 --duration=10 \
-o h100_window \
python inference.py
```
### ncu (Nsight Compute) -- Kernel Profiling
```bash
# Full kernel analysis
ncu --set full \
--target-processes all \
--launch-skip 10 --launch-count 5 \
-o h100_kernel_profile \
python inference.py
# Memory throughput analysis
ncu --metrics \
dram__bytes_read.sum,\
dram__bytes_write.sum,\
dram__throughput.avg.pct_of_peak_sustained,\
l1tex__t_bytes_pipe_lsu_mem_global_op_ld.sum,\
l2__read_throughput.avg.pct_of_peak_sustained \
python inference.py
# Occupancy analysis
ncu --metrics \
sm__warps_active.avg.pct_of_peak_sustained,\
sm__maximum_warps_per_active_cycle,\
launch__occupancy \
python inference.py
# Instruction mix analysis
ncu --metrics \
smsp__inst_executed.sum,\
smsp__inst_executed_pipe_fp32.sum,\
smsp__inst_executed_pipe_fp16.sum,\
smsp__inst_executed_pipe_tensor.sum \
python inference.py
```
### Key Metrics to Monitor
| Metric | Target | Description |
|---|---|---|
| DRAM Throughput | > 70% of 3.35 TB/s | Memory bandwidth utilization |
| SM Occupancy | > 50% | Active warps vs maximum |
| L2 Hit Rate | > 50% | Data reuse in L2 cache |
| Shared Memory Throughput | > 60% | Shared memory bandwidth utilization |
| Achieved FLOPs | > 40% of peak | Compute utilization |
| Warp Stall Reasons | < 20% any single | Identify bottlenecks |
### Profiling Workflow
1. **Start with nsys** to get the big picture (timeline, kernel durations)
2. **Identify hotspot kernels** from the nsys output
3. **Profile hotspots with ncu** for detailed analysis
4. **Check memory throughput** -- most DL kernels are memory-bound
5. **Check occupancy** -- ensure you are not register/shared-memory limited
6. **Iterate**: optimize, reprofile, compare
## Summary
When optimizing for H100:
1. **Leverage 132 SMs** -- size grids as multiples of 132
2. **Saturate 3.35 TB/s bandwidth** -- vectorized 128-bit loads/stores are essential
3. **Use 192 KB shared memory** for maximum data reuse in tiled algorithms
4. **Prefer BF16** for its wider dynamic range, matching modern model precision
5. **Use warp shuffles** for intra-warp communication to avoid shared memory overhead
6. **Implement online softmax** for memory-efficient attention
7. **Fuse operations** (RMSNorm+linear, GEGLU) to reduce memory round-trips
8. **Profile with nsys then ncu** to identify and fix bottlenecks
9. **Balance occupancy vs. register usage** -- 50% occupancy is often sufficient for compute-bound kernels
|