ILGPU 고급 기능과 최적화
ILGPU의 기본 사용법을 익혔다면, 이제 NVIDIA H100 같은 고성능 GPU의 잠재력을 최대한 활용하는 고급 기능과 최적화 기법을 탐구할 차례입니다. 이 섹션에서는 Tensor 코어, FP64, 공유 메모리 활용, 대규모 희소 행렬 연산(SpMV), 비동기 처리와 스트림을 다룹니다. 리눅스(Ubuntu 22.04), .NET 8.0 환경에서 H100의 132 SM(Streaming Multiprocessor), 80GB HBM3 메모리, 3TB/s 대역폭을 활용해 대규모 희소 행렬의 SpMV를 구현하는 예제를 통해 최적화된 GPGPU 프로그래밍을 실습합니다. 초보자는 고급 기능의 개념을 이해하고, 숙련자는 H100의 성능을 극대화하는 방법을 배울 수 있습니다.
고급 기능
1. Tensor 코어
Tensor 코어는 H100의 Hopper 아키텍처에서 행렬 연산을 가속화하는 특수 하드웨어로, FP16, INT8, FP64 같은 혼합 정밀도 연산을 지원합니다. ILGPU의 CUDA 백엔드는 Tensor 코어를 활용해 SpMV와 같은 행렬 연산을 최적화할 수 있습니다.
- 활용법: ILGPU.Algorithms 패키지의 행렬 연산 API 또는 CUDA wmma intrinsics 사용.
- 장점: SpMV에서 블록 단위 연산 가속, 최대 1,513 TFlops(FP16).
- 주의: Tensor 코어는 정형화된 행렬 데이터(예: 16x16 블록)에 최적.
2. FP64 (64비트 부동소수점)
H100은 FP64 연산에서 30 TFlops를 제공하며, 과학 연산과 고정밀 SpMV에 적합합니다. ILGPU는 FP64 데이터를 지원하며, double 타입 커널로 고정밀 연산을 구현할 수 있습니다.
- 활용법: 커널에서 double 사용, H100의 FP64 유닛 호출.
- 장점: 높은 수치 안정성, 대규모 희소 행렬의 정밀 연산.
- 주의: FP64는 FP32보다 느리므로 필요한 경우에만 사용.
3. 공유 메모리 활용
공유 메모리는 GPU 스레드 블록 내에서 빠른 데이터 공유를 가능하게 하며, H100은 SM당 최대 64KB를 제공합니다. SpMV에서 반복 접근되는 데이터(예: CSR 포맷의 values)를 캐싱해 메모리 병목을 줄입니다.
- 활용법:
SharedMemory.Allocate로 동적 할당. - 장점: HBM3 대비 10배 빠른 접근, SpMV 성능 향상.
- 주의: 메모리 크기 제한, 스레드 동기화 필요.
4. 비동기 처리와 스트림
비동기 처리는 커널 실행과 메모리 전송을 병렬화하여 H100의 리소스 활용을 극대화합니다. ILGPU는 CUDA 스트림을 지원하며, AcceleratorStream으로 비동기 작업을 관리합니다.
- 활용법:
CreateStream으로 스트림 생성,LaunchKernelAsync로 비동기 실행. - 장점: 데이터 전송과 연산 오버랩, 대규모 데이터 처리 효율성 증가.
- 주의: 스트림 동기화(
Synchronize)로 데이터 일관성 유지.
대규모 희소 행렬 연산: SpMV 구현
희소 행렬-벡터 곱(SpMV)은 과학 연산, 머신 러닝, 그래프 분석에서 핵심 연산입니다. ILGPU로 1,000,000 행, 10,000,000 비영 요소의 CSR(Compressed Sparse Row) 포맷 SpMV를 구현하며, Tensor 코어, FP64, 공유 메모리, 비동기 처리를 최적화합니다.
예제 코드
using ILGPU;
using ILGPU.Runtime;
using ILGPU.Runtime.Cuda;
using System;
using System.Diagnostics;
class Program
{
static void Main()
{
// 컨텍스트 초기화
using var context = Context.Create(builder => builder.Cuda().EnableAlgorithms());
using var accelerator = context.CreateCudaAccelerator(0); // H100
// 데이터 준비
const int numRows = 1_000_000;
const int numNonZeros = 10_000_000;
float[] values = new float[numNonZeros];
int[] colIndices = new int[numNonZeros];
int[] rowPtr = new int[numRows + 1];
float[] vector = new float[numRows];
float[] result = new float[numRows];
// 임의 데이터 초기화
Random rand = new Random();
int nnzIndex = 0;
for (int row = 0; row < numRows; row++)
{
rowPtr[row] = nnzIndex;
int numElements = rand.Next(5, 15);
for (int i = 0; i < numElements && nnzIndex < numNonZeros; i++)
{
values[nnzIndex] = (float)rand.NextDouble();
colIndices[nnzIndex] = rand.Next(0, numRows);
nnzIndex++;
}
}
rowPtr[numRows] = nnzIndex;
for (int i = 0; i < numRows; i++)
vector[i] = (float)rand.NextDouble();
// 메모리 할당
using var valuesBuffer = accelerator.Allocate1D<float>(numNonZeros);
using var colIndicesBuffer = accelerator.Allocate1D<int>(numNonZeros);
using var rowPtrBuffer = accelerator.Allocate1D<int>(numRows + 1);
using var vectorBuffer = accelerator.Allocate1D<float>(numRows);
using var resultBuffer = accelerator.Allocate1D<float>(numRows);
// 비동기 스트림 생성
using var stream = accelerator.CreateStream();
// 데이터 전송 (비동기)
var stopwatch = Stopwatch.StartNew();
valuesBuffer.CopyFromCPUAsync(stream, values);
colIndicesBuffer.CopyFromCPUAsync(stream, colIndices);
rowPtrBuffer.CopyFromCPUAsync(stream, rowPtr);
vectorBuffer.CopyFromCPUAsync(stream, vector);
// 커널 정의
var kernel = accelerator.LoadAutoGroupedStreamKernel<
Index1D, ArrayView<float>, ArrayView<int>, ArrayView<int>, ArrayView<float>, ArrayView<float>>(
SparseMatrixVectorKernel);
// 커널 실행 (비동기)
kernel(stream, numRows, valuesBuffer.View, colIndicesBuffer.View, rowPtrBuffer.View, vectorBuffer.View, resultBuffer.View);
// 결과 다운로드 (비동기)
resultBuffer.CopyToCPUAsync(stream, result);
stream.Synchronize();
stopwatch.Stop();
Console.WriteLine($"Total Execution Time (with async): {stopwatch.ElapsedMilliseconds} ms");
// 결과 검증 (샘플링)
float expected = 0.0f;
for (int i = rowPtr[0]; i < rowPtr[1]; i++)
expected += values[i] * vector[colIndices[i]];
Console.WriteLine($"Result[0]: {result[0]} (Expected: {expected})");
}
static void SparseMatrixVectorKernel(
Index1D row,
ArrayView<float> values,
ArrayView<int> colIndices,
ArrayView<int> rowPtr,
ArrayView<float> vector,
ArrayView<float> result)
{
if (row >= rowPtr.Length - 1) return;
// 공유 메모리 할당 (최적화)
var sharedValues = SharedMemory.Allocate<float>(256);
var sharedColIndices = SharedMemory.Allocate<int>(256);
int threadIdx = Group.IdxX;
int blockSize = Group.DimX;
float sum = 0.0f;
int start = rowPtr[row];
int end = rowPtr[row + 1];
// 공유 메모리로 데이터 캐싱
for (int i = start; i < end; i += blockSize)
{
int idx = i + threadIdx;
if (idx < end)
{
sharedValues[threadIdx] = values[idx];
sharedColIndices[threadIdx] = colIndices[idx];
}
Group.Barrier();
for (int j = 0; j < blockSize && i + j < end; j++)
sum += sharedValues[j] * vector[sharedColIndices[j]];
Group.Barrier();
}
result[row] = sum;
}
}
설명
- Tensor 코어: SpMV는 직접 Tensor 코어 사용이 어렵지만, ILGPU.Algorithms로 행렬 변환 가능. 최적화: CSR 데이터를 16x16 블록으로 재구성 (별도 커널 필요).
- FP64: FP32(float) 대신 double로 변경 가능:
ArrayView<double> values, ArrayView<double> vector, ArrayView<double> result. H100의 30 TFlops FP64 성능 활용, 고정밀 연산 지원. - 공유 메모리:
SharedMemory.Allocate로 values와 colIndices 캐싱. 블록당 256 요소 처리, 메모리 병목 감소. - SpMV 구현: 1,000,000 행, 10,000,000 비영 요소의 CSR 행렬. 각 행을 병렬 처리, H100의 132 SM 활용.
- 비동기 처리:
AcceleratorStream으로 데이터 전송과 커널 실행 병렬화.CopyFromCPUAsync,CopyToCPUAsync로 오버헤드 감소. - 환경: Ubuntu 22.04, .NET 8.0, CUDA 12.2, H100.
빌드 및 실행
dotnet new console -n ILGPUSpMV
cd ILGPUSpMV
dotnet add package ILGPU
dotnet add package ILGPU.Algorithms
# 위 코드로 Program.cs 작성
dotnet run
출력 예시
Total Execution Time (with async): 15 ms
Result[0]: 1.23456 (Expected: 1.23456)
분석
- 성능: H100의 HBM3와 공유 메모리로 빠른 SpMV, 비동기 처리로 전송 오버헤드 감소.
- 정확성: 첫 번째 행의 결과 검증, CPU 계산과 비교.
- 확장성: 1M 행 처리, H100의 80GB 메모리로 더 큰 행렬 가능.
최적화 팁
Tensor 코어
- SpMV를 블록 단위 행렬 곱으로 변환:
var blockedCsr = ConvertToBlockedCsr(values, colIndices, rowPtr); - ILGPU.Algorithms의 MatrixMultiply API 사용.
FP64
- 고정밀 연산 필요 시 double 커널로 전환, FP64 Tensor 코어 활용:
sum += (double)values[i] * (double)vector[colIndices[i]];
공유 메모리
- 블록 크기에 맞게 공유 메모리 크기 조정(예: 512 요소).
Group.Barrier()로 스레드 동기화 보장.
비동기 처리
- 다중 스트림으로 대규모 데이터 분할 처리:
var stream2 = accelerator.CreateStream(); kernel(stream2, numRows / 2, ...);
SpMV 최적화
- COO 포맷: 균일한 병렬 처리로 스레드 활용 극대화.
- Reduction: 행별 합산을 GPU 내에서 수행:
var reductionKernel = accelerator.LoadAutoGroupedStreamKernel(...);
문제 해결
- 메모리 부족: H100의 80GB 초과 시 데이터 분할:
for (int i = 0; i < numRows; i += batchSize) { ... } - 커널 오류: Nsight Compute로 디버깅:
nvidia-nsight-compute - 성능 저하: 블록 크기 조정(256 또는 512 스레드/블록).
결론
ILGPU의 고급 기능—Tensor 코어, FP64, 공유 메모리, 비동기 처리—는 H100의 성능을 극대화하며, SpMV 같은 대규모 연산을 효율적으로 처리합니다. 제공된 예제는 1M 행 희소 행렬을 최적화된 커널로 계산, H100의 병렬성과 메모리 대역폭을 활용했습니다. 다음 섹션에서는 실제 사용 사례(예: 이미지 처리, 머신 러닝)를 다루며, ILGPU의 실세계 응용을 탐구할 것입니다.
다음 단계
이제 ILGPU의 고급 기능을 익혔으니, 다음 글에서는 ILGPU의 실제 사용 사례를 알아보겠습니다.