신경망 엔진(Neural Network Engine, NNE) 은 다양한 신경망 런타임(실행 제공자라고도 함)에 액세스하고 런타임별 코딩 없이 신경망을 평가할 수 있는 공통 API를 제공합니다. 언리얼 엔진에는 해당 플러그인을 활성화하여 프로젝트에 추가할 수 있는 개별 런타임이 포함되어 있습니다. 최선의 런타임을 동적으로 선택하여, 사용하는 모델과 타깃 하드웨어에 따라 프로젝트의 NNE 에셋에서 신경망을 생성하고 실행할 수 있습니다.
NNE는 서로 다른 사용 사례를 지원하기 위해 런타임별로 구현되는 다양한 인터페이스를 제공합니다. 이러한 사용 사례마다 다른 방식으로 신경망을 실행해야 할 수 있습니다. 해당 인터페이스에 따라 CPU에서 실행할지, GPU에서 실행할지, 아니면 프레임 렌더링에 맞춰 GPU에서 실행할지가 결정됩니다. 이 구현을 통해 신경망 모델이 프로젝트에서 실행될지 여부와 그 방식을 최대한 제어할 수 있습니다.
NNE를 사용하여 실시간 추론을 통해 인공 지능(Artificial Intelligence, AI)으로 게임을 보강하고 에디터 기반 기능을 에셋 작업, 쿼리, 아티스트 지원 툴로 구현할 수 있습니다.
다음은 NNE를 구성하는 핵심 엘리먼트입니다.
- 인터페이스(Interfaces): NNE 런타임 플러그인에서 액세스할 수 있는 부분입니다.
- 런타임(Runtimes): 프로젝트에서 하나 이상의 인터페이스 구현에 사용되는 플러그인입니다.
- 에셋(Assets): 런타임이 활성화된 경우 언리얼 엔진에 임포트할 수 있는 신경망 모델 에셋입니다.
- 모델(Models): 신경망 모델 에셋 내에 저장되는 데이터입니다.
인터페이스
인터페이스는 NNE 런타임 플러그인에서 액세스할 수 있는 부분입니다. API를 깔끔하고 명확하게 유지하기 위해 런타임에 직접 액세스하는 대신 런타임이 구현하는 인터페이스 클래스를 사용하며, 이 인터페이스가 해당 런타임에 대한 API를 노출합니다. 각 인터페이스는 하나의 명확하게 정의된 사용 사례를 위해 고안되었으며, 런타임은 해당 타입의 사용 사례를 평가할 수 있는지 여부에 따라 인터페이스를 구현합니다.
사용 사례의 예로는 INNERuntimeCPU 및 INNERuntimeGPU 를 사용하여 모델 평가를 GPU의 프레임 렌더링에 맞추기 위해 CPU에서 동기식 추론을 호출하는 것을 들 수 있습니다. INNERuntimeRDG 는 렌더 종속성 그래프(FRDGBuilder)를 사용하여 비동기적으로 큐에 등록됩니다. NNE는 다양한 인터페이스를 노출하며, 각 인터페이스는 API를 깔끔하고 명확하게 유지하기 위해 한 가지 종류의 사용 사례를 지원하도록 설계되었습니다.
이러한 일관성 덕분에 런타임 개발자는 각 인터페이스에 해당하는 함수 기능을 지원하는 데 집중할 수 있습니다. 향후 하위 호환성을 유지하면서 새로운 종류의 사용 사례를 지원하기 위해 새 인터페이스가 추가될 수 있습니다.
다음과 같은 인터페이스를 사용할 수 있습니다.
| 인터페이스 이름 | 설명 |
|---|---|
INNERuntimeCPU |
CPU에서 추론이 발생하는 모든 사용 사례를 지원하는 인터페이스입니다. CPU에서 입력 및 출력 텐서가 제공되며 다른 디바이스에 메모리를 전송하지 않습니다. GPU에 예산이 없거나 GPU 메모리와의 동기화가 계산 속도 향상을 정당화하지 않는 사용 사례에 적합합니다. 이 인스턴스에서 생성된 모델 인스턴스는 게임 스레드(동기식)에서 비동기 작업으로 실행되거나, 호출자가 입력 및 출력 메모리를 포함하여 모델의 스레드 세이프 및 메모리 수명을 관리하는 한 다른 스레드에서 실행될 수 있습니다. |
INNERuntimeGPU |
GPU에서 신경망을 평가하는 런타임을 지원하는 인터페이스입니다. 입력 및 출력 텐서가 CPU 메모리로 제공되며, 런타임이 입력 및 출력 텐서를 업로드하고 다운로드하는 GPU와의 동기화가 필요합니다. 추론은 프레임 렌더링과는 독립적으로 발생하지만, GPU 리소스를 놓고 렌더링 파이프라인과 경쟁합니다. 이 인터페이스는 일반적으로 추가 GPU 동기화 및 리소스 경쟁이 퍼포먼스에 영향을 주지 않는 에디터 전용 사용 사례에 적합합니다. INNERuntimeCPU 와 마찬가지로 이 인터페이스에서 생성된 모델 인스턴스는 호출자가 스레드 세이프 및 메모리 수명을 관리하는 한 어떤 스레드에서든 실행될 수 있습니다. |
INNERuntimeRDG |
제공된 RDG 그래프 빌더(RDG Graph Builder)에 모델 평가를 추가하는 렌더 종속성 그래프(Render Dependency Graph, RDG)의 일부로 신경망 평가를 지원하는 인터페이스입니다. 이 인터페이스는 프레임을 렌더링하는 동안 사용되는 리소스를 신경망이 소비하고 생성할 때 사용됩니다. 추론은 GPU에서 수행되며, 입력 및 출력 텐서는 RDG 버퍼로 제공되어야 합니다. 추론은 렌더 스레드에서 호출되지만, 모델 생성 및 구성은 일반적으로 게임 스레드에서 수행됩니다. 이를 통해 엔진 리소스와의 긴밀한 통합이 가능합니다. |
런타임
런타임(Runtimes) 은 NNE 인터페이스를 구현하는 플러그인입니다. 런타임은 일반적으로 시작 시 NNE에 자체 등록됩니다.
TArray<FString>UE::NNE::GetAllRuntimeNames() 함수를 사용하여 런타임이 어떤 인터페이스를 구현하든 상관없이 사용할 수 있는 모든 런타임의 이름을 가져올 수 있습니다.
TArray<FString>UE::NNE::GetAllRuntimeNames<T>() 템플릿 함수를 사용하여 지정된 인터페이스를 구현하는 사전 필터링된 런타임 이름 목록을 가져올 수 있습니다. 예를 들어, CPU에서 발생하는 인터페이스를 구현하는 모든 런타임을 가져오려면, UE::NNE::GetAllRuntimeNames<INNERuntimeCPU>() 함수를 사용합니다.
런타임을 얻으려면 TWeakInterfacePtr<INNERuntime>UE::NNE::GetRuntime(const FString& Name) 함수를 사용합니다. 또는, TWeakInterfacePtr<T>UE::NNE::GetRuntime<T>(const FString& Name) 을 사용해 해당하는 템플릿 함수를 얻을 수 있습니다.
해당하는 플러그인이 활성화된 경우에도 모든 플랫폼에서 모든 런타임을 사용할 수 있는 것은 아닙니다. 런타임을 사용할 수 없거나, 사용할 수 있어도 템플릿 함수에서 전달된 인터페이스를 구현하지 않는 경우, 반환된 위크 포인터는 null이 됩니다. 런타임은 자체 언로드될 수 있으므로 사용하기 전에 위크 포인터의 유효성 테스트를 실행해야 합니다.
런타임은 일반적으로 관련 플러그인 및 모듈과 함께 자체 등록/등록 해제, 로드/언로드될 수 있습니다. 하지만, 런타임의 수명 및 등록은 런타임별 구현에 달려 있습니다.
에셋
런타임 플러그인이 해당 파일 타입을 지원하면, NNE 플러그인을 통해 신경망 모델 파일을 언리얼 엔진으로 직접 임포트할 수 있습니다. 언리얼 엔진은 임포트된 신경망 모델을 위해 콘텐츠 브라우저에 NNE 모델 데이터(NNE Model Data) 에셋을 생성합니다.
모든 런타임이 모든 파일 포맷을 지원하는 것은 아닙니다. 파일 타입은 성공적으로 임포트된 것으로 표시되어도 그 특정 런타임에 해당하는 모델 생성은 여전히 실패할 수 있습니다. 지원되는 파일 포맷에 대해서는 사용하는 런타임을 참조하세요.
임포트에 성공하면 엔진이 UNNEModelData 에셋을 생성합니다. 콘텐츠 브라우저에서 해당 에셋을 열어 프로젝트에서 구현된 특정 런타임에 대한 모델을 활성화하거나 비활성화할 수 있습니다. 필요 없는 런타임을 제거하면 프로젝트의 패키징 속도가 빨라지고 패키지 크기는 줄어듭니다. 기본적으로 각 모델은 각 런타임에 최적화되어 있지만, 사용하려는 모델이 있는 런타임만 선택하는 것이 좋습니다.
언리얼 엔진이 NNE 모델 데이터를 로드하는 방식은 다른 UE 에셋을 로드하는 방식과 같습니다. 예를 들어, UPROPERTY 데코레이터가 있는 UNNEModelData 타입의 퍼블릭 클래스 변수를 액터 내부에 정의하고 에디터에서 거기에 모델을 할당하면 해당 액터가 스폰될 때 자동으로 로드됩니다. 또한, 콘텐츠 경로가 알려진 경우 언리얼 엔진 함수 LoadObject 를 사용하여 프로그래밍 방식으로 에셋을 로드할 수도 있습니다.
모델
모델(Model) 은 인터페이스 내에 포함되며 로드된 UNNEModelData 에셋을 사용하여 런타임에 의해 구현됩니다. 다음 함수를 사용하여 에셋에서 모델을 생성할 수 있습니다.
TSharedPtr<UE::NNE:IModelCPU> INNERuntimeCPU::CreateModelCPU(const TObjectPtr<UNNEModelData> ModelData)TSharedPtr<UE::NNE:IModelGPU> INNERuntimeGPU::CreateModelGPU(const TObjectPtr<UNNEModelData> ModelData)TSharedPtr<UE::NNE:IModelRDG> INNERuntimeRDG::CreateModelRDG(const TObjectPtr<UNNEModelData> ModelData)
모든 런타임이 모든 NNE 모델 데이터 에셋에서 모델을 생성할 수 있는 것은 아닙니다. 런타임은 언리얼 에디터 내에서 쿠킹을 위한 요구 사항으로 사용할 수 있지만, 현재 플랫폼에서 모델이 추론을 실행하지 못할 수도 있습니다. 다음 함수를 사용하여 모델 생성 가능 여부를 나타내는 상태를 반환할 수 있습니다.
ECanCreateModelCPUStatus INNERuntimeCPU::CanCreateModelCPU(const TObjectPtr<UNNEModelData> ModelData) constECanCreateModelGPUStatus INNERuntimeGPU::CanCreateModelGPU(const TObjectPtr<UNNEModelData> ModelData) constECanCreateModelRDGStatus INNERuntimeRDG::CanCreateModelRDG(const TObjectPtr<UNNEModelData> ModelData) const
이러한 함수를 호출한 다음에 모델을 생성해야 합니다. 결과가 모델을 생성할 수 있다고 나오더라도 내부 오류로 인해 실제 생성이 실패할 수 있습니다. 사용하기 전에 항상 반환된 모델에 대한 공유 포인터의 유효성을 확인해야 합니다.
생성된 모델에는 일반적으로 변경 불가 버퍼가 모델의 다양한 인스턴스 간에 공유되는 모델 가중치 및 파라미터로 포함됩니다. 모델이 내부적으로 필요한 데이터에 대한 포인터를 유지하므로 모델 생성 후에 UNNEModelData를 릴리즈할 수 있습니다.
모델 인스턴스
다음 함수 중 하나를 통해 신경망 모델에서 모델 인스턴스(Model Instance) 를 생성하여 추론을 실행할 수 있습니다.
TSharedPtr<UE::NNE::IModelInstanceCPU> UE::NNE::IModelCPU::CreateModelInstanceCPU()TSharedPtr<UE::NNE::IModelInstanceGPU> UE::NNE::IModelGPU::CreateModelInstanceGPU()TSharedPtr<UE::NNE::IModelInstanceRDG> UE::NNE::IModelRDG::CreateModelInstanceRDG()
모델 인스턴스에는 일반적으로 세션별 데이터가 내부 상태 및 중간 버퍼로 포함됩니다. 가중치 및 파라미터로 모델의 변경 불가 데이터를 공유하는 단일 모델에서 여러 인스턴스를 생성할 수 있습니다. 그런 다음 인스턴스가 생성된 후에 이 모델을 릴리즈할 수 있는데, 해당 인스턴스가 모든 필요한 공유 데이터에 대한 자체 레퍼런스를 유지하기 때문입니다.
첫 번째 추론을 호출하기 전에 모델에 적절한 크기의 내부 버퍼가 할당될 수 있도록 다음 함수를 호출해야 합니다.
UE::NNE::ESetInputTensorShapesStatus SetInputTensorShapes(TConstArrayView<UE::NNE::FTensorShape> InInputShapes)
런타임은 임포트 또는 쿠킹 시간에 포착하지 못한 모든 잠재적 오류를 보고하며, 사용자는 반환된 상태 코드를 확인해야 합니다. 입력 셰이프가 변경될 때마다 해당 함수를 다시 호출해야 합니다. 불필요한 반복 호출을 방지하여 계산 리소스 오버헤드를 낮게 유지하세요.
호출자는 모든 NNE 모델 인스턴스의 입력 및 출력 메모리를 소유합니다. 호출자는 전체 추론 호출 내내 입력 및 출력 텐서의 메모리가 본래 그대로 유효하게 유지되도록 해야 합니다. 스레드 사용 사례에서는 여기에 특별한 주의가 필요합니다.
틱당 또는 프레임당 여러 번 모델 인스턴스를 평가해야 하는 경우 여러 차례 개별 호출을 실행하는 것보다 일괄적으로 평가하는 것이 더 퍼포먼스가 좋으므로 입력 데이터를 일괄처리하는 것이 좋습니다. 일괄처리를 통해 여러 호출을 동기화할 수 없는 경우, NNE에서 요구하는 스레드 세이프를 위반하지 않고 여러 인스턴스가 동시에 추론을 실행할 수 있습니다.
최소 NNE 예시
다음과 같은 필수 포인트와 코드 스니펫 예시를 사용하여 CPU에서 신경망 사용을 시작할 수 있습니다.
이 예시에서는 간단히 읽어보기 쉽도록 입력 및 출력 텐서를 준비하고 구성하는 방법에 대해서는 자세하게 설명하지 않습니다. 또한, 이 예시에서는 어떠한 결과도 확인하지 않지만, 실제 사용 사례에서는 확인해야 합니다.
시작하는 방법에 대한 자세한 내용은 신경망 엔진 퀵스타트 가이드를 참조하세요.
시작하는 방법은 다음과 같습니다.
- 언리얼 엔진의 플러그인(Plugins) 브라우저에서 NNE 런타임 플러그인을 활성화합니다.
- 프로젝트의
.Build.cs파일에NNE모듈을 종속성으로 추가합니다. - 콘텐츠 브라우저를 통해
*.onnx파일 타입 같은 신경망 파일을 언리얼 엔진에 임포트합니다.
다음 코드를 사용하여 모델을 로드하고 실행합니다.
// NNE 헤더를 포함합니다.
#include “NNE.h”
#include “NNERuntimeCPU.h”
#include “NNEModelData.h”
// 신경망 모델 데이터 에셋에서 모델을 생성합니다.
TObjectPtr<UNNEModelData> ModelData = LoadObject<UNNEModelData>(GetTransientPackage(), TEXT("/path/to/asset"));
TWeakInterfacePtr<INNERuntimeCPU> Runtime = UE::NNE::GetRuntime<INNERuntimeCPU>(FString("NNERuntimeORTCpu"));
TSharedPtr<UE::NNE::IModelInstanceCPU> ModelInstance = Runtime->CreateModelCPU(ModelData)->CreateModelInstanceCPU();
// TODO: 입력 및 출력 텐서와 텐서 바인딩, 해당 입력 셰이프를 구성합니다.
// 특정 입력 크기가 주어진 모델을 준비합니다.
ModelInstance->SetInputTensorShapes(InputShapes);
// 호출자가 소유한 CPU 메모리를 전달하는 모델을 실행합니다.
ModelInstance->RunSync(Inputs, Outputs);