Unity Sentis로 ONNX 모델을 C# 런타임에서 온디바이스 추론하는 방법

Unity Sentis에서 ONNX 모델을 임포트하고 C#으로 입력 텐서를 만들어 GPU·CPU 백엔드에서 추론한 뒤 결과를 게임플레이에 연결하는 구현 절차와 메모리·성능 최적화 방법을 정리합니다.

✓ 복사 완료!
챕터일반 가이드
난이도중급
상태● 검토 완료
대상 엔진
Unity
최종 업데이트
Unity Sentis로 ONNX 모델을 C# 런타임에서 온디바이스 추론하는 방법

핵심 요약

Unity Sentis는 학습이 끝난 ONNX 모델을 Unity 앱 내부에서 실행하는 추론 라이브러리다. ModelAsset을 런타임 Model로 읽고 IWorker에 입력 Tensor<float>를 전달한 뒤 출력을 읽으면 서버 호출 없이 게임플레이, UI, 비전 기능에 모델 결과를 연결할 수 있다.

핵심은 학습 당시의 입력 이름·형상·전처리와 동일한 텐서를 만들고 IWorker와 텐서를 매 프레임 새로 생성하지 않으며 대상 기기에서 CPU와 GPU 백엔드를 프로파일링하는 것이다.

Unity Sentis와 ONNX 온디바이스 추론은 무엇인가?

ONNX(Open Neural Network Exchange)는 PyTorch, TensorFlow 등에서 학습한 모델을 교환하기 위한 형식이다. Unity Sentis는 .onnx 파일을 ModelAsset으로 임포트해 플레이어 기기에서 실행한다. 즉, 학습 파이프라인은 Unity 밖에서 유지하고 게임 클라이언트는 추론만 담당한다.

Unity 6 계열에서는 Sentis 패키지 com.unity.ai.inference 2.4.1이 제공된다. 패키지와 네임스페이스는 버전에 따라 달라질 수 있으므로 이 글의 실행 코드는 Unity.Sentis 네임스페이스를 사용하는 Sentis 2.x API를 기준으로 한다. 최신 패키지에서 컴파일 오류가 난다면 설치된 패키지의 마이그레이션 문서와 샘플 API를 먼저 확인한다.

항목ONNX 학습 모델Unity Sentis 런타임
담당 작업학습한 연산 그래프와 가중치 보관모델 로드와 추론 실행
실행 위치Python 학습 환경 또는 모델 저장소Unity Editor와 최종 플레이어 기기
입력모델이 정의한 텐서 형상과 데이터 타입C#에서 만든 Tensor<float> 등
출력로짓, 회귀값, 특징 벡터 등PeekOutput()으로 얻는 출력 텐서

온디바이스 추론은 네트워크 왕복 지연과 서버 비용을 줄이고 오프라인 동작을 가능하게 한다. 반면 모델 파일 크기, 기기별 성능 차이, 지원되지 않는 ONNX 연산자는 클라이언트에서 검증해야 한다.

Unity Sentis ONNX 모델을 어떻게 준비할까?

1. 입력 계약을 먼저 고정한다

모델을 Unity로 가져오기 전에 다음 정보를 학습 코드나 Netron 같은 ONNX 뷰어에서 기록한다.

  1. 입력과 출력 텐서 이름
  2. 데이터 타입. 일반적인 이미지·수치 모델은 float32다.
  3. 형상. 이미지 분류 모델은 흔히 [1, 3, 224, 224], 수치 분류 모델은 [1, featureCount]를 사용한다.
  4. 전처리. RGB/BGR 순서, 0~1 정규화, 평균·표준편차, 채널 순서 NCHW/NHWC를 확인한다.
  5. 출력 해석. 확률인지 softmax 이전 로짓인지 회귀값인지 구분한다.

예를 들어 학습 시 이미지 값 x를 0~255에서 0~1 범위로 바꿨다면 런타임도 같은 변환을 적용해야 한다.

xnormalized=x255x_{normalized} = \frac{x}{255}

평균과 표준편차를 사용한 모델이라면 채널별로 다음 식을 적용한다.

xstandardized=x/255−μσx_{standardized} = \frac{x / 255 - \mu}{\sigma}

2. ONNX를 Unity 에셋으로 임포트한다

.onnx 파일을 Assets/Models/ 같은 프로젝트 폴더에 넣는다. Unity가 이를 ModelAsset으로 임포트한다. Inspector에서 모델의 입력과 출력 이름, 형상, 지원 경고를 확인한다. 경고가 있다면 런타임까지 미루지 말고 해당 연산을 지원되는 ONNX 연산으로 내보내거나 모델을 단순화한다.

Unity Inspector에서 ONNX ModelAsset의 입력과 출력 정보를 확인하는 화면

3. 패키지와 실행 백엔드를 선택한다

Package Manager에서 Sentis를 설치한 뒤 using Unity.Sentis;를 추가한다. 백엔드는 첫 구현에서는 BackendType.GPUCompute를 시도하고 지원 기기와 실제 프레임 시간을 기준으로 BackendType.CPU도 비교한다. GPU가 항상 빠른 것은 아니다. 작은 모델은 GPU 작업 제출과 읽기 대기 비용 때문에 CPU가 더 적합할 수 있다.

상황우선 검토할 백엔드이유
수백 개 수준의 작은 수치 입력BackendType.CPUGPU 전송·동기화 비용이 상대적으로 클 수 있음
이미지 모델, 합성곱 연산이 많은 모델BackendType.GPUCompute병렬 연산 처리에 유리함
저사양 또는 GPU Compute 제약 기기BackendType.CPU호환성 확인이 쉬움
결과를 즉시 CPU 게임 로직에서 사용둘 다 프로파일링출력 읽기 비용까지 포함해 비교해야 함

C# 런타임 추론은 어떻게 구현할까?

아래 예제는 길이 8의 수치 특징 벡터를 입력받아 가장 높은 점수의 클래스를 반환한다. Inspector에서 modelAsset에 임포트한 ONNX 에셋을 연결한다. 모델의 실제 입출력 이름과 형상은 예제의 8, 3이 아니라 자신의 모델 계약으로 바꿔야 한다.

using System;
using Unity.Sentis;
using UnityEngine;

public sealed class SentisClassifier : MonoBehaviour
{
    [SerializeField] private ModelAsset modelAsset;

    private Model runtimeModel;
    private IWorker worker;

    private void Awake()
    {
        runtimeModel = ModelLoader.Load(modelAsset);
        worker = WorkerFactory.CreateWorker(BackendType.GPUCompute, runtimeModel);
    }

    public int Predict(float[] features)
    {
        if (features == null || features.Length != 8)
            throw new ArgumentException("모델 입력은 float 8개여야 합니다.", nameof(features));

        using var input = new Tensor<float>(new TensorShape(1, 8), features);
        worker.Execute(input);

        using var output = worker.PeekOutput() as Tensor<float>;
        var scores = output.DownloadToArray();
        return ArgMax(scores);
    }

    private static int ArgMax(float[] values)
    {
        var bestIndex = 0;
        for (var i = 1; i < values.Length; i++)
            if (values[i] > values[bestIndex]) bestIndex = i;
        return bestIndex;
    }

    private void OnDestroy()
    {
        worker?.Dispose();
    }
}

실행 흐름은 짧다. ModelLoader.Load()는 임포트된 에셋을 런타임 모델로 만들고 WorkerFactory.CreateWorker()는 실행기를 만든다. Execute()가 그래프를 실행하며 PeekOutput()은 결과 텐서를 가리킨다. DownloadToArray()는 CPU 배열이 필요한 시점에만 호출한다.

flowchart LR
    A[ONNX ModelAsset] --> B[ModelLoader.Load]
    B --> C[IWorker 생성]
    D[C# float 배열] --> E[Tensor<float> 생성]
    E --> C
    C --> F[출력 Tensor]
    F --> G[점수·행동·UI 반영]

이미지 ONNX 모델의 전처리는 왜 자주 틀릴까?

이미지 모델 실패의 대부분은 Sentis 자체보다 전처리 불일치에서 발생한다. 모델이 [1, 3, 224, 224] NCHW와 RGB를 기대하는데 Unity 텍스처의 픽셀을 NHWC 또는 BGR로 넣으면 실행은 성공해도 예측값은 틀릴 수 있다.

다음은 Texture2D에서 RGB 값을 읽어 NCHW float32 배열을 만드는 최소 예제다. 입력 텍스처는 이미 224×224로 리사이즈되어 있다고 가정한다.

private static float[] ToNchwRgb(Texture2D texture)
{
    const int width = 224;
    const int height = 224;
    var pixels = texture.GetPixels32();
    var data = new float[3 * width * height];

    for (var y = 0; y < height; y++)
    for (var x = 0; x < width; x++)
    {
        var i = y * width + x;
        var pixel = pixels[i];
        data[i] = pixel.r / 255f;
        data[width * height + i] = pixel.g / 255f;
        data[2 * width * height + i] = pixel.b / 255f;
    }

    return data;
}

이 배열은 다음처럼 입력 텐서에 넣는다.

using var input = new Tensor<float>(
    new TensorShape(1, 3, 224, 224),
    ToNchwRgb(inputTexture));
worker.Execute(input);

학습에 평균과 표준편차가 사용됐다면 pixel.r / 255f 자리에 해당 채널의 정규화 식을 적용한다. 또한 Texture2D.GetPixels32()는 CPU 읽기 비용이 있으므로 카메라 프레임 전체를 매 프레임 분류하는 구조에는 적합하지 않을 수 있다.

Unity 카메라 텍스처를 모델의 NCHW RGB 입력 텐서로 변환하는 전처리 과정

Sentis 추론 성능과 메모리는 어떻게 최적화할까?

IWorker는 한 번 만들고 재사용한다

모델 로드와 워커 생성은 초기화 단계에서 한 번만 실행한다. 입력 내용만 바뀌는 매 프레임 처리에서 ModelLoader.Load()나 CreateWorker()를 반복하면 초기화 비용과 메모리 변동이 커진다.

출력 다운로드를 필요한 순간으로 미룬다

DownloadToArray()는 GPU 출력이 CPU에 필요할 때 동기화를 유발할 수 있다. 결과가 화면 효과나 후속 GPU 처리로만 이어진다면 CPU 배열로 읽는 설계를 피할 수 있는지 먼저 검토한다. 게임 로직에 클래스 인덱스 하나만 필요하다면 추론 빈도를 낮추는 편이 가장 단순하고 효과적이다.

모든 프레임에서 실행하지 않는다

NPC 행동 선택, 음성 명령, 이미지 분류는 보통 렌더 프레임마다 갱신할 필요가 없다. 예를 들어 60 FPS 게임에서 0.2초마다 추론하면 초당 5회만 실행한다.

private float nextInferenceTime;

private void Update()
{
    if (Time.time < nextInferenceTime) return;

    nextInferenceTime = Time.time + 0.2f;
    var predictedClass = classifier.Predict(currentFeatures);
    ApplyDecision(predictedClass);
}

프레임 예산으로 판단한다

목표 프레임 속도가 60 FPS라면 프레임 전체 예산은 약 16.67 ms다.

frame budget=1000target FPSframe\ budget = \frac{1000}{target\ FPS}

Sentis만 따로 빠르게 보이는지보다 입력 전처리, Execute(), 출력 읽기, 게임플레이 반영까지 포함한 시간을 Unity Profiler에서 측정한다. 모바일과 PC, 개발 빌드와 릴리스 빌드의 결과도 구분한다.

자주 묻는 질문 (FAQ)

Sentis는 모델 학습도 할 수 있나?

아니다. Sentis는 Unity 앱에서 모델을 실행하는 추론용 라이브러리다. 학습은 일반적으로 PyTorch나 TensorFlow 같은 외부 환경에서 수행하고 ONNX로 내보낸다.

ONNX 파일을 넣었는데 Unity에서 임포트 경고가 난다. 실행해도 되나?

권장하지 않는다. 경고는 지원되지 않는 연산자나 형상 처리 문제일 수 있다. 먼저 Inspector 경고와 모델의 ONNX opset을 확인하고 지원되는 연산으로 재내보낸 뒤 테스트한다.

GPU Compute를 쓰면 항상 더 빠른가?

아니다. 작은 모델이나 CPU에서 즉시 결과를 읽어야 하는 경우에는 CPU가 더 빠를 수 있다. 배포 대상 기기에서 전처리와 결과 읽기까지 포함해 측정한 뒤 선택한다.

정리

Unity Sentis 온디바이스 추론 구현은 ONNX 모델을 ModelAsset으로 임포트하고 입력 계약에 맞는 텐서를 만들어 재사용 워커로 실행하는 흐름이다. 먼저 작은 수치 모델로 입력·출력 검증을 끝낸 다음, 이미지 전처리와 실행 주기 조절, CPU·GPU 프로파일링을 추가하면 게임 클라이언트에 안정적으로 적용할 수 있다.

패키지 버전별 API 차이는 있으므로 실제 프로젝트에서는 Unity의 Sentis 패키지 매뉴얼과 설치된 패키지의 샘플을 함께 확인한다.