ILGPU 기본 사용법

ILGPU를 사용해 GPU 병렬 연산을 시작하려면 커널 작성, 메모리 관리, 실행 흐름을 이해하는 것이 중요합니다. 이 섹션에서는 ILGPU의 기본 사용법을 다루며, 간단한 커널 작성 및 실행, 메모리 할당과 데이터 전송, 디버깅과 성능 프로파일링에 초점을 맞춥니다. NVIDIA H100 GPU와 리눅스(Ubuntu 22.04), .NET 8.0 환경을 중심으로, 2D 배열의 요소별 제곱을 계산하는 예제를 통해 ILGPU의 핵심 기능을 실습합니다. 초보자는 기본 작업 흐름을 익히고, 숙련자는 디버깅과 성능 최적화 기법을 탐구할 수 있습니다.

간단한 커널 작성 및 실행

커널은 ILGPU에서 GPU 병렬 연산을 정의하는 핵심 요소로, C# 메서드처럼 작성됩니다. 커널은 GPU의 수천 개 스레드에서 병렬 실행되며, Index1D 또는 Index2D를 사용해 스레드별 작업을 지정합니다. ILGPU는 커널을 JIT(Just-In-Time) 컴파일하여 CUDA PTX로 변환, H100 같은 GPU에서 효율적으로 실행합니다.

커널 작성 요령

  • 인덱싱: Index1D는 1D 배열, Index2D는 2D 배열에 적합.
  • 간결성: 복잡한 로직은 여러 커널로 분리.
  • 제한: 커널 내에서는 .NET 표준 라이브러리(예: Console.WriteLine) 사용 불가.

커널 실행

커널은 LoadAutoGroupedStreamKernel 메서드로 로드되며, 액셀러레이터에서 실행됩니다. 실행 시 스레드 수(그리드/블록 크기)를 지정해 H100의 병렬성을 활용합니다.

메모리 할당과 데이터 전송

ILGPU는 GPU 메모리 관리와 CPU-GPU 간 데이터 전송을 간소화합니다. H100의 80GB HBM3 메모리와 3TB/s 대역폭을 활용하려면 효율적인 메모리 할당과 전송이 필수입니다.

메모리 할당

  • Allocate1D/2D: 1D 또는 2D 배열을 GPU 메모리에 할당.
  • ArrayView: GPU 메모리 접근을 위한 래퍼, 안전한 인덱싱 제공.
  • 스코프 관리: using으로 메모리 해제 자동화.

데이터 전송

  • CopyFromCPU: CPU에서 GPU로 데이터 업로드.
  • CopyToCPU: GPU에서 CPU로 결과 다운로드.
  • 최적화: 전송 최소화, GPU 내 연산 최대화.

디버깅과 성능 프로파일링

ILGPU 코드를 안정적으로 개발하고 최적화하려면 디버깅과 성능 분석이 중요합니다. 디버깅은 CPU 백엔드로 검증하고, 성능 프로파일링은 H100의 연산 효율을 극대화합니다.

디버깅

  • CPU 백엔드: GPU 없이 커널 실행, Visual Studio 디버거로 단계별 검증.
  • 유효성 검사: 입력/출력 데이터 비교, 경계 조건 확인.
  • 로그: Console.WriteLine으로 중간 결과 출력(커널 외부).

성능 프로파일링

  • 타이밍: Stopwatch로 커널 실행 시간 측정.
  • NVIDIA 도구: Nsight Systems/Compute로 H100의 커널 실행, 메모리 병목 분석.
  • 최적화 지표: 스레드 블록 크기, 메모리 접근 패턴, Tensor 코어 활용.

예제: 2D 배열의 요소별 제곱 계산

ILGPU의 기본 사용법을 실습하기 위해, 2D 배열의 각 요소를 제곱하는 커널을 작성하고 실행합니다. 이 예제는 H100의 CUDA 백엔드를 사용하며, 메모리 관리, 디버깅, 프로파일링을 포함합니다.

예제 코드

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().CPU());
        using var cudaAcc = context.CreateCudaAccelerator(0); // H100
        using var cpuAcc = context.CreateCPUAccelerator(0); // CPU (디버깅용)

        // 데이터 준비
        const int rows = 1000;
        const int cols = 1000;
        float[,] input = new float[rows, cols];
        Random rand = new Random();
        for (int i = 0; i < rows; i++)
            for (int j = 0; j < cols; j++)
                input[i, j] = (float)rand.NextDouble();

        // CUDA 실행 및 프로파일링
        float[] cudaResult = RunKernel(cudaAcc, input, "CUDA");

        // CPU 실행 (디버깅용)
        float[] cpuResult = RunKernel(cpuAcc, input, "CPU");

        // 결과 검증
        ValidateResults(cudaResult, cpuResult, input);
    }

    static float[] RunKernel(Accelerator accelerator, float[,] input, string backend)
    {
        int rows = input.GetLength(0);
        int cols = input.GetLength(1);
        int size = rows * cols;

        // 1D 배열로 변환 (GPU 전송용)
        float[] inputFlat = new float[size];
        for (int i = 0; i < rows; i++)
            for (int j = 0; j < cols; j++)
                inputFlat[i * cols + j] = input[i, j];

        // 메모리 할당
        using var inputBuffer = accelerator.Allocate1D<float>(size);
        using var outputBuffer = accelerator.Allocate1D<float>(size);

        // 데이터 업로드
        inputBuffer.CopyFromCPU(inputFlat);

        // 커널 정의 및 로드
        var kernel = accelerator.LoadAutoGroupedStreamKernel<
            Index2D, ArrayView<float>, ArrayView<float>>(
            (index, input, output) => { 
                output[index.X * input.Length / input.Height + index.Y] = 
                    input[index.X * input.Length / input.Height + index.Y] * 
                    input[index.X * input.Length / input.Height + index.Y]; 
            });

        // 프로파일링
        var stopwatch = Stopwatch.StartNew();
        kernel(new Index2D(rows, cols), inputBuffer.View, outputBuffer.View);
        accelerator.Synchronize();
        stopwatch.Stop();
        Console.WriteLine($"{backend} Kernel Execution Time: {stopwatch.ElapsedMilliseconds} ms");

        // 결과 다운로드
        float[] result = new float[size];
        outputBuffer.CopyToCPU(result);

        return result;
    }

    static void ValidateResults(float[] cudaResult, float[] cpuResult, float[,] input)
    {
        int rows = input.GetLength(0);
        int cols = input.GetLength(1);
        bool isValid = true;
        for (int i = 0; i < rows; i++)
        {
            for (int j = 0; j < cols; j++)
            {
                int idx = i * cols + j;
                float expected = input[i, j] * input[i, j];
                if (Math.Abs(cudaResult[idx] - expected) > 1e-5 || 
                    Math.Abs(cpuResult[idx] - expected) > 1e-5)
                {
                    isValid = false;
                    Console.WriteLine($"Validation Failed at [{i},{j}]: " +
                        $"CUDA={cudaResult[idx]}, CPU={cpuResult[idx]}, Expected={expected}");
                    break;
                }
            }
            if (!isValid) break;
        }
        if (isValid)
            Console.WriteLine("Validation Passed: CUDA and CPU results match expected values.");
    }
}

설명

  • 커널 작성 및 실행: 커널은 2D 배열의 각 요소를 제곱, Index2D로 스레드 인덱싱. LoadAutoGroupedStreamKernel로 로드, H100에서 병렬 실행.
  • 메모리 할당과 데이터 전송: Allocate1D로 GPU 메모리 할당, ArrayView로 접근. CopyFromCPU로 입력 데이터 업로드, CopyToCPU로 결과 다운로드.
  • 디버깅과 프로파일링: CPU 백엔드로 동일 커널 실행, 결과 검증. Stopwatch로 실행 시간 측정, Nsight 권장.
  • 환경: Ubuntu 22.04, .NET 8.0, CUDA 12.2, H100.
  • 데이터: 1000x1000 2D 배열, HBM3 메모리 활용.

빌드 및 실행

dotnet new console -n ILGPUSquare2D
cd ILGPUSquare2D
dotnet add package ILGPU
# 위 코드로 Program.cs 작성
dotnet run

출력 예시

CUDA Kernel Execution Time: 2 ms
CPU Kernel Execution Time: 50 ms
Validation Passed: CUDA and CPU results match expected values.

분석

  • CUDA 백엔드: H100의 병렬 스레드와 HBM3 메모리로 빠른 연산.
  • CPU 백엔드: 디버깅용, CUDA 결과와 비교해 정확성 검증.
  • 프로파일링: CUDA는 CPU보다 수십 배 빠름, Nsight로 추가 분석 가능.

추가 팁

커널 작성

  • 복잡한 연산은 여러 커널로 분리, 중간 결과 저장.
  • Index2D 사용 시 배열 크기와 스레드 매핑 확인.

메모리 관리

  • 대규모 데이터는 GPU 내 연산 우선, 전송 최소화.
  • H100의 HBM3 활용: Allocate1D로 큰 배열 할당 가능.

디버깅

Visual Studio Code에서 CPU 백엔드 디버깅:

{
    "name": ".NET Core Launch",
    "type": "coreclr",
    "request": "launch",
    "program": "${workspaceFolder}/bin/Debug/net8.0/ILGPUSquare2D"
}
  • 경계 오류 확인: if (index.X < rows && index.Y < cols) 추가.

프로파일링

  • Nsight Systems: nsight-sys
  • 커널 실행 시간, 메모리 전송 병목 분석.
  • 최적화: Tensor 코어 활용(ILGPU.Algorithms), 스레드 블록 크기 조정(256 스레드/블록).

결론

ILGPU의 기본 사용법은 커널 작성, 메모리 관리, 디버깅, 프로파일링으로 구성됩니다. 2D 배열 제곱 예제는 H100의 CUDA 백엔드 성능과 CPU 백엔드 디버깅을 보여주며, .NET 8.0 환경에서 ILGPU의 간결함을 강조합니다. 다음 섹션에서는 고급 기능(예: Tensor 코어, 희소 행렬 연산)을 다루며, H100의 잠재력을 극대화하는 최적화 기법을 탐구할 것입니다.

다음 단계

이제 ILGPU의 기본 사용법을 익혔으니, 다음 글에서는 ILGPU의 고급 기능을 알아보겠습니다.

ILGPU 시리즈 요약으로 돌아가기