ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • DGX Spark에서 ONNX Runtime GPU 빌드 성공기 — 8번의 실패와 1번의 성공
    IT 2026. 3. 16. 21:00

    들어가며

    22,500장의 가족 사진을 AI로 분석하고 싶었다. CLIP으로 자연어 검색을, InsightFace로 얼굴 인식을 돌리면 된다. 문제는 GPU 없이 CPU로 돌리면 며칠이 걸린다는 것이었다.

    NVIDIA DGX Spark라면 128GB 통합 메모리에 Blackwell 아키텍처 GB10 GPU까지 있으니 충분히 빠르게 돌릴 수 있을 것 같았다. 하지만 GPU를 쓰기까지 8번의 실패를 거쳐야 했다. 이것은 그 기록이다.


    환경

    항목
    서버 NVIDIA DGX Spark
    CPU NVIDIA Grace (ARM64 / aarch64)
    GPU NVIDIA GB10 (Blackwell, SM 121, compute 12.1)
    메모리 128GB LPDDR5x (CPU/GPU 통합)
    CUDA 13.0.88
    Driver 580.126.09
    OS Ubuntu 24.04

    왜 GPU가 안 되는가

    CLIP이나 InsightFace 같은 모델을 GPU에서 실행하려면 ONNX Runtime의 GPU 버전(onnxruntime-gpu)이 필요하다. 그런데 PyPI의 onnxruntime-gpu에 ARM64(aarch64) wheel이 없다. x86_64만 제공한다.

    결론: ONNX Runtime GPU를 소스에서 직접 빌드해야 한다.

    여기서부터 대서사가 시작된다.


    시도 1: CUDA 12.8에서 SM 121로 빌드

    가장 직관적인 접근. 현재 설치된 CUDA 12.8 개발 이미지에서 GB10(SM 121)을 타겟으로 빌드한다.

    그 전에, 빌드해야 할 대상이 무엇인지를 이해해야 한다. NVIDIA가 이미 제공하는 것과 우리가 직접 빌드해야 하는 것이 구분된다.

    NVIDIA가 제공하는 것 (이미 빌드됨)

    Docker 이미지 nvidia/cuda:12.8.0-cudnn-devel에는 다음이 포함되어 있다:

    • nvcc 컴파일러 — GPU용 코드를 컴파일하는 도구. gcc가 C 코드를 CPU 바이너리로 변환하듯, nvcc는 CUDA 코드를 GPU 바이너리로 변환한다.
    • libcudart — CUDA 런타임 라이브러리. GPU 메모리 할당, 데이터 전송 등 기본 기능.
    • libcublas — 행렬 곱셈 등 선형대수 연산. 이미 최적화된 GPU 커널이 내장되어 있다.
    • libcudnn — 딥러닝 연산(컨볼루션, 배치 정규화 등). 역시 미리 컴파일된 GPU 커널 포함.

    이것들은 NVIDIA가 미리 빌드해서 제공하는 완성된 도구와 라이브러리다.

    우리가 빌드해야 하는 것

    ONNX Runtime 소스 코드에는 GPU에서 실행할 CUDA C++ 코드(.cu 파일)가 수백 개 있다:

    • onnxruntime/core/providers/cuda/math/concat.cu — 텐서 합치기 연산
    • onnxruntime/core/providers/cuda/nn/conv.cu — 컨볼루션 연산
    • onnxruntime/contrib_ops/cuda/bert/attention.cu — 어텐션 연산
    • ... 등 수백 개의 GPU 커널

    .cu 파일들은 소스 코드다. CLIP이나 InsightFace 모델을 GPU에서 실행할 때, 내부적으로 이 커널들이 호출된다. 이것들을 nvcc 컴파일러로 빌드해야 한다.

    빌드 과정과 결과물

    diagram

    ./build.sh --use_cuda를 실행하면 CMake가 수백 개의 .cu 파일을 nvcc에 넘기면서 "SM 121용으로 컴파일해줘"라고 지시한다.

    FROM nvidia/cuda:12.8.0-cudnn-devel-ubuntu22.04
    # ...
    cmake -DCMAKE_CUDA_ARCHITECTURES=121 ...
    

    결과

    빌드:  .cu 소스 → nvcc 12.8 → ✗ 컴파일 실패
    산출물: 없음
    

    에러

    nvcc fatal : Unsupported gpu architecture 'compute_121'
    

    첫 번째 .cu 파일을 만나자마자 바로 에러가 발생했다.

    원인

    CUDA 12.8의 nvcc 컴파일러는 SM 121(Blackwell)을 모른다. SM 121은 CUDA 13.0부터 추가된 아키텍처다. nvcc 버전이 타겟 GPU 아키텍처를 알아야 컴파일할 수 있는데, 12.8은 그 이전에 릴리즈되었다. "SM 121이 뭔지 모르겠다"고 거부한 것이다.

    교훈

    GPU 아키텍처와 CUDA 툴킷 버전은 짝이 맞아야 한다. Blackwell(SM 121)은 CUDA 13.0 이상 필수.


    시도 2: CUDA 13.0 + ORT v1.23.2

    그러면 CUDA 13.0 이미지를 사용하면 된다. ARM64용 nvidia/cuda:13.0.0-cudnn-devel-ubuntu24.04 이미지가 존재하는 것을 확인했다.

    FROM nvidia/cuda:13.0.0-cudnn-devel-ubuntu24.04
    # ONNX Runtime v1.23.2
    git clone --branch v1.23.2 https://github.com/microsoft/onnxruntime.git
    

    결과

    빌드:  .cu 소스 → nvcc 13.0 → 컴파일 중 → ✗ 헤더 참조 실패
    산출물: 없음
    

    에러

    cutlass/fast_math.h:42:10: fatal error: cuda/std/utility: No such file or directory
    

    원인

    이 에러를 이해하려면 ONNX Runtime 내부의 종속 관계를 알아야 한다:

    diagram

    ONNX Runtime은 GPU 행렬 연산을 위해 CUTLASS를 사용하고, CUTLASS는 CUDA의 C++ 표준 라이브러리인 CCCL의 헤더 파일을 참조한다. 문제는 CUDA 13.0에서 CCCL 헤더의 디렉토리 구조가 변경되었다는 것이다. cuda/std/utility 파일이 다른 위치로 이동했는데, ORT v1.23.2에 번들된 구 버전 CUTLASS는 이전 경로를 그대로 참조하고 있었다.

    CUDA 12.8:  cuda/std/utility  ← 여기에 있음 ✓
    CUDA 13.0:  cuda/std/utility  ← 이동됨, 없음 ✗
    

    즉, ONNX Runtime → CUTLASS → CCCL 체인에서, 맨 아래 CCCL의 변경이 위로 전파된 것이다.

    교훈

    ONNX Runtime 버전과 CUDA 버전 사이에도 호환성 제약이 있다. 종속 라이브러리 체인 전체가 맞아야 한다. v1.23.2는 CUDA 13.0을 지원하지 않는다.


    시도 3: contrib ops 비활성화로 CUTLASS 우회

    CUTLASS 에러가 contrib_ops/cuda/llm/cutlass_heuristic.cc에서 발생했다. 에러 위치가 "contrib_ops"(기여 연산) 디렉토리 안이라는 점이 중요하다.

    소프트웨어에서 빌드 에러가 특정 모듈에서 발생할 때, 해당 모듈을 비활성화하는 것은 흔히 시도하는 방법이다. 리눅스 커널 빌드에서 문제되는 드라이버를 끄거나, CMake 프로젝트에서 옵션 기능을 꺼서 빌드하는 것과 같은 접근이다. "contrib_ops"라는 이름 자체가 "핵심이 아닌 추가 기능"이라는 뉘앙스를 풍기기 때문에, 이걸 빼면 CUTLASS 없이도 빌드될 것이라고 기대했다.

    ./build.sh --use_cuda --disable_contrib_ops ...
    

    결과

    빌드:  .cu 소스 → nvcc 13.0 → 컴파일 성공 → ✗ 링크 실패
    산출물: 없음
    

    에러

    undefined reference to `onnxruntime::GetFusedActivationAttr`
    

    원인

    ONNX Runtime의 핵심 코드(core 디렉토리의 fp16_conv.cc)가 contrib ops에 정의된 함수 GetFusedActivationAttr()를 참조하고 있었다. --disable_contrib_ops는 contrib ops의 빌드를 건너뛰지만, core 코드에서 해당 심볼을 링크하려고 시도하기 때문에 링커 에러가 발생한다.

    diagram

    "contrib"이라는 이름은 "선택적 추가 기능"을 연상시키지만, 실제로는 ONNX Runtime의 핵심 코드가 이 모듈에 의존하고 있었다. 추가 기능인 줄 알았는데 사실상 필수 모듈이었던 것이다. 이름만 보고 판단한 것이 실수였다.

    교훈

    모듈 이름이 "optional" 느낌이라고 해서 실제로 optional인 것은 아니다. 의존성 그래프를 확인하지 않고 비활성화하면 링커 에러를 만날 수 있다.


    시도 4: CUDA 12.8 + SM 90 PTX — 큰 방향 전환

    여기서 근본적으로 전략을 바꿨다. 시도 1~3의 흐름을 정리하면:

    시도 1: CUDA 12.8 + SM 121 → nvcc가 SM 121 모름
    시도 2: CUDA 13.0 + SM 121 → ORT 1.23.2가 CUDA 13.0 비호환
    시도 3: CUDA 13.0 + contrib ops 제거 → 핵심 코드 의존으로 불가
    

    "CUDA 13.0 + SM 121"이라는 정공법이 막혔다. 그래서 완전히 다른 접근을 시도했다: CUDA 12.8에 머무르면서, SM 121이 아닌 SM 90(Hopper)의 PTX를 생성하는 것이다.

    PTX forward compatibility란?

    "forward compatibility"라는 이름이 처음에는 헷갈릴 수 있다. 누구 관점에서 "forward"인 걸까?

    PTX(중간 코드) 관점에서 생각하면 이해된다. PTX는 과거에 만들어진 코드다. 이 과거의 코드가 미래의(= 아직 존재하지 않았던) GPU에서도 실행될 수 있다는 것이 forward compatibility다. PTX 입장에서 "앞으로(forward) 나올 GPU에서도 호환(compatible)된다"는 의미다.

    diagram

    핵심은 네이티브 코드(SASS)는 forward compatibility가 안 되지만, 중간 코드(PTX)는 될 수 있다는 것이다. PTX는 GPU 세대에 독립적인 가상 명령어셋이기 때문에, 새 GPU의 드라이버가 이를 해당 GPU에 맞는 네이티브 코드로 JIT 컴파일할 수 있다.

    diagram

    이 계획이 성공하면 CUDA 13.0이 필요 없다. CUDA 12.8의 nvcc로 SM 90 PTX를 만들고, 실행은 CUDA 13.0 드라이버가 알아서 해주니까.

    FROM nvidia/cuda:12.8.0-cudnn-devel-ubuntu24.04
    cmake -DCMAKE_CUDA_ARCHITECTURES=90 ...
    

    결과

    빌드:  .cu 소스 → nvcc 12.8 → SM 90 PTX → ✓ wheel 생성 성공!
    배포:  wheel 설치 → ✗ 실행 환경에서 로딩 실패
    산출물: onnxruntime_gpu-1.23.2-cp311-linux_aarch64.whl (빌드 성공, 실행 불가)
    

    빌드는 성공했지만, 실행 환경에서 로딩조차 되지 않았다.

    에러

    ImportError: /usr/lib/aarch64-linux-gnu/libm.so.6: version `GLIBC_2.38' not found
    

    원인: GLIBC backward compatibility

    GLIBC(GNU C Library)는 Linux에서 거의 모든 프로그램이 사용하는 핵심 시스템 라이브러리다. GLIBC는 backward compatibility를 보장한다 — 새 버전의 GLIBC는 구 버전에서 빌드된 바이너리를 실행할 수 있다. 하지만 그 반대는 안 된다:

    diagram

    스마트폰 앱에 비유하면 이해하기 쉽다. "Android 14 이상 필요"라고 빌드된 앱은 Android 13 기기에서 설치할 수 없다. 플랫폼(실행 환경)의 버전이 앱(빌드된 바이너리)이 요구하는 버전보다 같거나 높아야 한다.

    [해결 방향]
    
      빌드: Ubuntu 22.04 (GLIBC 2.35) ← 더 낮은 버전에서 빌드
      실행: Debian 12    (GLIBC 2.36)
      결과: 2.36 ≥ 2.35 → ✓ 실행 가능!
    

    교훈

    빌드 환경의 GLIBC 버전은 실행 환경보다 같거나 낮아야 한다. 최신 OS에서 빌드하면 구 환경에서 실행할 수 없다.


    시도 5: Ubuntu 22.04로 재빌드 — 빌드 성공, 런타임 실패

    GLIBC 문제를 해결하기 위해 Ubuntu 22.04(GLIBC 2.35) 기반으로 변경.

    FROM nvidia/cuda:12.8.0-cudnn-devel-ubuntu22.04
    cmake -DCMAKE_CUDA_ARCHITECTURES=90
    # ORT v1.23.2
    

    결과

    빌드:  .cu 소스 → nvcc 12.8 → SM 90 PTX → ✓ wheel 생성 성공!
    배포:  wheel 설치 → ✓ CUDAExecutionProvider 등록 성공!
    실행:  모델 inference → ✗ GPU 커널 실행 실패
    산출물: onnxruntime_gpu-1.23.2-cp311-linux_aarch64.whl (빌드·배포 성공, 런타임 실패)
    

    가장 가까이 갔다! CUDAExecutionProvider가 목록에 나타났다. 하지만 실제로 모델을 돌리면...

    에러

    CUDA error cudaErrorSymbolNotFound: named symbol not found
    

    Concat 노드에서 에러가 발생한다.

    원인

    시도 4에서 계획한 PTX forward compatibility가 작동하지 않았다. 그 이유는 우리가 빌드한 코드뿐 아니라 CUDA 런타임 라이브러리 내부에도 GPU 커널이 있기 때문이다:

    diagram

    CUDA_FORCE_PTX_JIT=1 환경변수도 시도했지만, 이것은 사용자 코드의 PTX에만 적용되고 라이브러리 내부 커널에는 영향을 주지 않았다.

    CUDA 12.9도 시도해봤지만 결과는 동일했다. CUDA 12.x 전체가 SM 121을 모른다.

    교훈

    PTX forward compatibility는 우리가 빌드한 코드에만 적용된다. CUDA 런타임 라이브러리(libcublas 등) 내부의 커널은 해당 라이브러리 버전이 그 GPU를 지원해야 한다. CUDA 12.x의 라이브러리에는 SM 121 커널이 없다.


    시도 6: CUDA 13.0 런타임 + soname 심링크

    시도 5에서 문제는 "CUDA 12.8 라이브러리에 SM 121 커널이 없다"는 것이었다. 그렇다면 아이디어가 떠오른다: ONNX Runtime은 CUDA 12.8로 빌드하되, 실행할 때는 CUDA 13.0의 런타임 라이브러리를 넣으면 어떨까? CUDA 13.0 라이브러리에는 SM 121 커널이 있으니까.

    diagram

    문제는 CUDA 12.8에서 빌드한 ONNX Runtime이 libcublas.so.12를 찾는데, CUDA 13.0은 libcublas.so.13을 제공한다는 것이다. 심링크를 만들어 우회를 시도했다.

    하지만 모든 라이브러리의 버전 변경을 파악해야 했다. 특히 libcufft: - CUDA 12.8: libcufft.so.11 - CUDA 13.0: libcufft.so.12

    결과

    빌드:  시도 5의 wheel 재사용 (CUDA 12.8로 빌드됨)
    배포:  CUDA 13.0 런타임 라이브러리 복사 + 심링크 → ✗ 라이브러리 로딩 실패
    산출물: 배포 환경 구성 실패
    

    에러

    libcufft.so.11: version 'libcufft.so.11' not found
    (required by libonnxruntime_providers_cuda.so)
    

    원인

    Linux 동적 링커(ld.so)는 파일명이 아닌 라이브러리 내부에 기록된 SONAME을 검증한다. 심링크로 파일명을 바꿔도 소용없다:

    diagram

    교훈

    동적 링커의 SONAME 검증을 심링크로 우회할 수 없다. CUDA 메이저 버전이 바뀌면 soname도 바뀌므로, 빌드 시점과 실행 시점의 CUDA 버전을 일치시켜야 한다.


    시도 7: ONNX Runtime v1.24.3으로 업그레이드

    여기서 근본적으로 다시 생각했다. 시도 5~6에서 배운 것을 종합하면:

    [문제 정리]
    
    1. CUDA 12.x 런타임 라이브러리에는 SM 121 커널이 없다
       → CUDA 13.0 런타임이 반드시 필요
    
    2. CUDA 13.0 런타임을 쓰려면, ONNX Runtime도 CUDA 13.0으로 빌드해야 한다
       → soname 불일치 때문에 버전 혼합 불가 (시도 6에서 확인)
    
    3. ONNX Runtime v1.23.2는 CUDA 13.0에서 빌드할 수 없다
       → CUTLASS 호환 문제 (시도 2에서 확인)
    
    결론: CUDA 13.0을 지원하는 새로운 ONNX Runtime 버전이 필요하다
    

    리서치 결과, ORT v1.24.2에서 CUDA 13.0 빌드 에러가 수정되었고, CUTLASS 4.2.1이 번들되어 SM 121을 지원한다는 것을 확인했다. 단, 이번에도 SM 90 PTX로 빌드했다:

    FROM nvidia/cuda:13.0.0-cudnn-devel-ubuntu22.04
    # ORT v1.24.3
    cmake -DCMAKE_CUDA_ARCHITECTURES=90
    

    결과

    빌드:  .cu 소스 → nvcc 13.0 → SM 90 PTX → ✓ wheel 생성 성공!
    배포:  wheel 설치 + CUDA 13.0 런타임 → ✓ CUDAExecutionProvider 등록 성공!
    실행:  모델 inference → ✗ GPU 커널 실행 실패
    산출물: onnxruntime_gpu-1.24.3-cp311-linux_aarch64.whl (빌드·배포 성공, 런타임 실패)
    

    빌드와 배포까지 성공! CUDAExecutionProvider도 확인! 그런데 모델 inference 시...

    에러

    CUDA error cudaErrorNoKernelImageForDevice: no kernel image is available for execution on the device
    

    원인

    시도 5와 비슷하지만 에러 메시지가 다르다. 이번에는 CUDA 13.0 런타임을 사용하므로 라이브러리 내부 커널은 SM 121을 알고 있다. 문제는 우리가 빌드한 ONNX Runtime 커널이다:

    diagram

    시도 5에서는 "라이브러리 커널이 SM 121을 모른다"였고, 시도 7에서는 "우리 커널이 SM 121에서 안 돌아간다"이다. 결국 SM 90 PTX로는 SM 121에서 실행할 수 없다는 것이 두 번째로 확인된 것이다.

    교훈

    SM 90(Hopper) → SM 121(Blackwell) 사이의 PTX forward compatibility는 작동하지 않는다. 세대 차이가 큰 아키텍처 간에는 네이티브 빌드가 필요하다.


    시도 8: CUDA 13.0 + SM 121 네이티브 — 최종 성공

    8번의 시도에서 배운 모든 것을 종합하면, 필요한 조건은 딱 두 가지:

    1. CUDA 13.0 — SM 121 컴파일 지원 + 런타임 SM 121 커널 포함
    2. ORT v1.24.3 — CUDA 13.0 호환 CUTLASS 4.2.1 번들
    FROM nvidia/cuda:13.0.0-cudnn-devel-ubuntu22.04
    
    RUN git clone --branch v1.24.3 https://github.com/microsoft/onnxruntime.git
    
    RUN ./build.sh \
        --use_cuda \
        --cuda_home /usr/local/cuda \
        --cudnn_home /usr \
        --cmake_extra_defines "CMAKE_CUDA_ARCHITECTURES=121" \
        --config Release \
        --build_wheel \
        --compile_no_warning_as_error
    

    결과

    빌드:  .cu 소스 → nvcc 13.0 → SM 121 네이티브 → ✓ wheel 생성 성공!
    배포:  wheel 설치 + CUDA 13.0 런타임 → ✓ CUDAExecutionProvider 등록 성공!
    실행:  모델 inference → ✓ GPU 가속 성공!
    산출물: onnxruntime_gpu-1.24.3-cp311-cp311-linux_aarch64.whl (53MB)
    

    빌드, 배포, 실행 모두 성공!


    최종 검증

    빌드한 wheel을 설치하고 GPU 프로바이더를 확인했다.

    $ python3 -c "import onnxruntime; print(onnxruntime.get_available_providers())"
    
    ['CUDAExecutionProvider', 'CPUExecutionProvider']
    

    드디어 CUDAExecutionProvider가 나타났다.

    22,500장의 사진에 대해 CLIP 검색 인덱싱을 실행했다. 약 4분 만에 완료. CPU 모드에서는 며칠이 걸렸을 작업이다. Face Detection과 Facial Recognition도 각각 8분, 10분 만에 완료. 에러 0건.


    성능 비교 요약

    작업 CPU 모드 GPU 모드
    CLIP 검색 인덱싱 (22,500장) 수일 4분
    InsightFace 얼굴 검출 수일 8분
    얼굴 클러스터링 수일 10분

    8번의 실패, 1번의 성공 — 전체 지도

    diagram


    핵심 교훈 정리

    1. GPU 아키텍처와 CUDA 버전은 짝

    GB10(SM 121)은 Blackwell 세대. CUDA 13.0부터 컴파일러와 런타임 모두 지원한다. CUDA 12.x에서는 어떤 방법으로도 SM 121을 쓸 수 없다.

    2. PTX forward compatibility를 과신하지 말 것

    "구 아키텍처 PTX를 새 GPU에서 JIT 컴파일한다"는 CUDA의 공식 메커니즘이지만, 세대 차이가 크면 작동하지 않을 수 있다. SM 90(Hopper) → SM 121(Blackwell) 사이에서 두 번 실패했다.

    3. 빌드 환경과 실행 환경의 GLIBC를 맞출 것

    높은 GLIBC에서 빌드한 바이너리는 낮은 GLIBC에서 실행할 수 없다. 실행 환경(플랫폼)의 GLIBC가 빌드 환경보다 같거나 높아야 한다.

    4. 동적 링커는 SONAME을 검증한다

    심링크로 라이브러리 파일명을 바꿔도, 동적 링커는 파일 내부의 SONAME 필드를 읽고 불일치를 거부한다. 빌드와 실행의 CUDA 버전을 일치시켜야 한다.

    5. ONNX Runtime과 CUDA의 호환 매트릭스를 확인할 것

    ORT 버전 CUDA 지원 CUTLASS Blackwell(SM 121)
    v1.23.2 ~12.8 구버전
    v1.24.2+ 13.0 4.2.1

    v1.24.2에서 CUDA 13.0 빌드 에러가 수정되었고, CUTLASS 4.2.1에 SM 121 지원이 추가되었다.


    마치며

    DGX Spark는 강력한 하드웨어지만, ARM64 + Blackwell이라는 조합이 아직 소프트웨어 생태계의 최전선에 있다 보니 기존 도구들이 바로 지원하지 않는 경우가 많다. 하지만 Docker와 소스 빌드를 활용하면 결국 해결할 수 있다.

    이 글에서 다룬 핵심 조합을 정리하면:

    • ONNX Runtime v1.24.3 — CUDA 13.0 + Blackwell 지원
    • CUDA 13.0 devel 이미지 — SM 121 컴파일러
    • CUDA 13.0 runtime 이미지 — SM 121 커널이 포함된 런타임 라이브러리
    • SM 121 네이티브 컴파일 — PTX가 아닌 네이티브 코드

    22,500장의 가족 사진이 4분 만에 검색 가능해졌다. "해변에서 뛰는 아이"라고 검색하면 진짜로 그 사진이 나온다. 이 하나를 위해 8번을 실패한 것이, 충분히 가치가 있었다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.