콘텐츠로 이동

실행 방식

이 문서는 device="rbln"의 동작과 device="cuda"와의 차이, 그 이유를 설명합니다. 기존 PyTorch 코드에 필요한 디바이스 선택, 컴파일, 메모리, CPU fallback, 동기화를 다루며 CUDA와 동작이 다른 경우에는 표에서 비교합니다. 설치를 마친 상태를 전제로 합니다. 개별 API와 연산자별 지원 범위는 API와 지원 연산에서, 같은 규칙의 실행 예제는 튜토리얼에서 확인할 수 있습니다.

주요 동작은 다음과 같습니다.

  • eager 실행도 컴파일합니다. 각 연산자를 실행 전에 shape와 dtype에 맞춰 컴파일한 뒤 캐시에 저장하고 재사용합니다. 첫 호출에는 컴파일 비용이 들지만 이후 호출에는 들지 않습니다.
  • 메모리는 지연 할당합니다. 디바이스 텐서를 만들 때 가상 주소를 예약하고 디바이스 연산이 처음 사용할 때 물리 디바이스 메모리를 할당합니다.
  • fallback은 실행을 이어가는 안전장치입니다. 디바이스에서 실행할 수 없는 작업은 CPU에서 실행하고 로그에 기록합니다. 범주별로 fallback 대신 오류가 발생하도록 설정할 수 있습니다.
  • tensor.device는 텐서가 속한 논리 디바이스를 나타냅니다. RBLN PyTorch는 NPU 메모리 할당과 데이터 전송을 필요할 때까지 미루며, 디바이스 실행 조건을 충족하지 않는 연산은 CPU fallback으로 처리할 수 있습니다. 따라서 rbln:0으로 표시되더라도 최신 데이터가 호스트 메모리에 있거나 일부 연산이 CPU에서 실행될 수 있습니다.

실행 모델

코드를 RBLN으로 옮기는 과정에는 두 가지 선택이 큰 영향을 미칩니다. 첫 번째는 텐서가 사용할 디바이스를 선택하는 것입니다. rbln:0 같은 인덱스는 PyTorch가 선택하는 단위인 논리 디바이스를 가리키며, 논리 디바이스와 RSD 그룹에서 설명하는 규칙에 따라 물리 NPU에 매핑됩니다. 두 번째는 연산자의 실행 방식입니다. eager는 각 연산자를 호출마다 개별적으로 처리하고 graph는 함수나 모듈의 연산자를 torch.compile로 함께 컴파일합니다. eager 디바이스 연산도 내부적으로 컴파일하므로 연산자의 첫 호출에는 준비 비용이 듭니다. 반복 실행을 측정할 때는 첫 호출과 이후 호출을 구분해야 합니다.

각 텐서는 .to("rbln")로 옮겨서 RBLN 디바이스에 배치할 수 있습니다. 실제 데이터 저장 위치와 연산자 실행 위치는 런타임의 메모리 관리와 연산자 지원 여부에 따라 결정됩니다. 할당은 디바이스, 텐서, 메모리에서, CPU 실행은 CPU fallback에서 설명합니다.

Rebel100™과 ATOM™

RBLN PyTorch는 Rebel100™(RBLN-CR 카드, 예: RBLN-CR13)과 ATOM™(RBLN-CA 카드) NPU에서 실행됩니다. 별도 설명이 없으면 이 문서의 내용은 두 제품군 모두에 적용됩니다. PyTorch 코드에서 나타나는 차이는 다음과 같습니다.

항목 ATOM™ Rebel100™
카드 칩 하나 칩렛 네 개로 구성된 칩 하나. 할당기는 칩렛마다 별도 풀을 유지하며 가장 많이 사용하는 칩렛의 메모리가 부족해지면 디바이스에 메모리 부족이 발생함
메모리 통계 디바이스별 디바이스별 통계와 memory_summary(), memory_stats_per_chiplet()의 칩렛별 통계
torch.profiler 호스트 활동만 표시 RBLN_PROFILER=1이면 Kineto 브리지를 통해 디바이스 이벤트를 트레이스에 표시(튜토리얼)
RSD 그룹 디바이스 매핑 변수로 여러 카드를 묶음. 텐서 병렬 실행 참고 카드 내부에 고정. 칩렛 네 개가 RSD-4 그룹을 이루며 컴파일러가 모든 프로그램에서 사용함. 프로그램 분할 방식은 RBLN Compiler 문서 참고. 디바이스 매핑 변수로 여러 카드를 묶지 않음
여러 카드 RSD 그룹 카드당 프로세스 하나와 논리 디바이스 하나를 사용하는 rbln-ccl(튜토리얼)

칩렛과 메모리 통계

컴파일러가 프로그램을 칩렛 네 개에 분할하는 방식은 RBLN Compiler 문서에서 설명합니다. RBLN PyTorch에서는 메모리 통계를 통해서만 칩렛을 확인할 수 있습니다. Rebel100™의 캐싱 할당기는 각 칩렛을 따로 추적합니다. memory_allocated(), memory_reserved(), memory_stats()는 칩렛별 사용량의 합계를 보여주므로 각 칩렛의 사용량 불균형을 알 수 없습니다. 반면 memory_summary()는 NPU와 칩렛마다 한 행을 출력하고 memory_stats_per_chiplet()는 memory_stats()와 같은 키에 npu.<n>.chiplet.<c>. 접두사를 붙여 반환합니다. 연산을 실행한 같은 Python 프로세스에서 print(torch.rbln.memory_summary())를 호출하면 칩렛별 메모리 사용량을 확인할 수 있습니다. 관련 API는 API와 지원 연산 › 메모리에서 확인할 수 있습니다. 카드 하나에서 float16 행렬 곱셈을 한 번 실행한 뒤의 출력은 다음과 같습니다.

torch_rbln memory summary (device=rbln:0, MiB)
scope: pid 461977 -- caching allocator only, this process only
npu: physical NPU id as in device_summary() (relative to RBLN_VISIBLE_DEVICES, may differ from rbln-smi)
  npu  chiplet    allocated   alloc peak     reserved    resv peak       active       cached
--------------------------------------------------------------------------------------------
    0        0          1.2          1.7          2.0          2.0          1.2          0.8
    0        1          0.0          0.0          2.0          2.0          0.0          2.0
    0        2          0.0          0.0          2.0          2.0          0.0          2.0
    0        3          0.0          0.0          2.0          2.0          0.0          2.0
--------------------------------------------------------------------------------------------
         total          1.3          1.8          8.0          8.0          1.3          6.7
alloc retries: 0   ooms: 0

최댓값은 칩렛별로 추적하므로 total 행의 최대 사용량 열은 전체 동시 최대 사용량의 상한입니다. 정확한 전체 최대 사용량은 memory_stats()에 있습니다. 메모리 사용량의 합계에 여유가 있어 보이는데 메모리 부족 오류가 발생한다면 칩렛 하나가 가득 찼을 수 있습니다. 이러한 경우는 이 표에서 확인할 수 있습니다.

Rebel100™에서 측정한 정렬 규칙

eager 디스패치의 두 가지 규칙은 연산자가 디바이스에 도달할 수 있는지를 결정합니다. 이 규칙은 하드웨어가 아닌 디스패치 계층에 구현되어 있으므로 ATOM™에도 적용됩니다. 이 문서의 측정은 Rebel100™을 기준으로 합니다.

  • 텐서 인자의 마지막 차원이 64의 배수가 아닌 eager 연산자는 CPU에서 실행됩니다. fallback 로그에는 연산자 이름이 표시되고 explain()에는 반복되는 dispatch/recompile로 나타납니다. 원소 3개의 add, Linear(128, 8) 출력, rotary embedding의 폭 32인 half-head 슬라이스가 모두 이 조건에 해당합니다. 마지막 차원을 패딩하거나 블록을 컴파일하면 이 fallback을 피할 수 있습니다.
  • 융합 attention 커널은 쿼리 시퀀스 길이가 32의 배수여야 합니다. 다른 길이는 sdpa_overrideable (query sequence length N not aligned to 32) op ran on CPU 메시지와 함께 fallback합니다. eager 생성에서는 단일 토큰 디코딩 단계마다 fallback합니다.

두 규칙은 연산자별로 적용되며 torch.compile 그래프 안에서는 컴파일러가 자체적으로 패딩하고 변환하므로 적용되지 않습니다. 전체 검사 순서는 디스패치 전 검사에서 확인할 수 있습니다.

디바이스, 텐서, 메모리

논리 디바이스와 RSD 그룹

물리 디바이스는 NPU입니다. 논리 디바이스는 PyTorch에서 rbln:N 같은 인덱스로 선택하는 단위입니다. 기본적으로 둘은 1:1로 매핑됩니다. Rebellions Scalable Design(RSD)을 사용하면 논리 디바이스 하나가 물리 NPU 2, 4, 8, 16, 32개를 묶고 컴파일된 프로그램을 그룹에 분할해 실행합니다. ATOM™에서는 RBLN_DEVICE_MAP 또는 RBLN_NPUS_PER_DEVICE로 그룹을 설정하고 RBLN_VISIBLE_DEVICES로 노출할 NPU를 지정합니다. 환경 변수 › 디바이스 매핑에서 확인할 수 있습니다. Rebel100™의 RSD 그룹은 카드 내부에 고정되어 있습니다. 칩렛 네 개가 RSD-4 그룹을 이루며 별도 설정 없이 모든 프로그램을 분할합니다. 논리 디바이스는 항상 카드 하나입니다. 여러 Rebel100™ 카드에 걸친 작업은 torch.distributed와 rbln-ccl을 사용합니다(분산 실행).

조회 API 반환 정보
torch.rbln.device_count() 논리 디바이스 수
torch.rbln.physical_device_count() 프로세스에 보이는 물리 NPU 수
torch.rbln.device_summary() 논리-물리 매핑 표를 표준 출력으로 표시. None 반환

ATOM™의 텐서 병렬 실행은 여러 NPU를 하나의 논리 디바이스로 묶어 설정합니다. 그룹은 연산자가 사용할 수 있는 NPU 범위를 정의합니다. 실제 요청 수는 실행 모드에 따라 결정되며 텐서 병렬 실행에서 설명합니다. 이 절의 나머지는 ATOM™ 설정에 관한 내용입니다. Rebel100™에서는 그룹 설정 변수를 설정해서는 안 됩니다.

예를 들어 ATOM™ NPU 네 개가 있는 호스트에서 다음 명령은 NPU를 두 개씩 묶습니다.

RBLN_NPUS_PER_DEVICE=2 python application.py
논리 디바이스 그룹에 속한 물리 NPU
rbln:0 0, 1
rbln:1 2, 3

애플리케이션은 rbln:0 또는 rbln:1을 선택합니다. device_count()는 2, physical_device_count()는 4를 반환합니다.

매핑은 다음 규칙을 따릅니다.

  • 프로세스에 보이는 NPU는 런타임의 RBLN_VISIBLE_DEVICES로 제한합니다(RBLN_DEVICES는 더 이상 권장하지 않는 별칭). 그룹 설정 변수와 device_summary()의 물리 NPU 번호는 이 목록의 인덱스입니다.
  • 그룹 설정 변수는 디바이스를 처음 실제로 사용하기 전에 설정해야 합니다. is_available(), device_count() 같은 조회 API는 매핑을 계획할 뿐 NPU를 점유하지 않습니다. 매핑은 할당, synchronize(), 집합 통신 등 최초의 실제 사용 시 확정됩니다. set_device()는 선택 정보만 기록하므로 확정하지 않습니다. 확정 후에는 변수 변경을 무시하며 프로세스가 종료될 때까지 매핑을 유지합니다. torch.cuda.device_count()도 CUDA 초기화 이후에만 결과를 캐시한다는 점에서 같습니다.
  • RBLN_DEVICE_MAP은 각 그룹의 NPU를 명시적으로 지정하며 RBLN_NPUS_PER_DEVICE보다 우선합니다.

CUDA에서는 사용자가 torch.distributed로 다중 GPU 모델을 분할합니다. RSD 그룹은 하나의 논리 디바이스이므로 RBLN Compiler가 컴파일된 그래프를 그룹에 분할하고 eager 코드에서는 디바이스 하나로 보입니다.

디바이스 선택

특정 논리 디바이스를 사용하려면 torch.device("rbln:N")처럼 인덱스를 지정합니다. 인덱스 없는 "rbln"은 현재 선택한 RBLN 디바이스를 뜻합니다. torch.accelerator를 사용하는 디바이스 독립 코드도 RBLN이 활성 가속기이면 rbln을 선택합니다. torch.rbln.device() 컨텍스트 관리자는 블록 안에서만 선택을 바꿉니다.

1
2
3
4
5
6
7
8
9
import torch

with torch.rbln.device(0):
    x = torch.tensor([1.0, 2.0], dtype=torch.float16, device="rbln")
    y = torch.tensor([3.0, 4.0], dtype=torch.float16, device="rbln")
    z = x + y

assert z.device == torch.device("rbln:0")
print(z.cpu())
tensor([4., 6.], dtype=torch.float16)

x, y, z는 모두 rbln:0에 배치됩니다. 컨텍스트 안에서 텐서를 만들 때도 device="rbln"을 지정해야 합니다. 컨텍스트는 팩토리 함수의 기본 디바이스를 바꾸거나 기존 텐서를 옮기지 않습니다. 기존 텐서는 .to(target_device)로 옮기고 같은 연산의 텐서는 하나의 논리 디바이스에 배치해야 합니다. RBLN 논리 디바이스 간 복사는 런타임이 처리하며 호스트 메모리를 거칠 수 있습니다.

프로세스와 fork()

이미 디바이스를 사용한 프로세스를 fork()해서는 안 됩니다. 자식 프로세스는 부모의 디바이스 상태를 상속하지만, 사용할 수 있는 런타임 컨텍스트는 상속하지 못합니다. CUDA는 fork된 자식 프로세스가 디바이스를 재초기화하면 오류를 발생시킵니다. 반면 RBLN PyTorch에는 잘못된 fork를 감지하는 기능이 없어, 자식 프로세스에서 오류가 발생하지 않더라도 안정적으로 동작하지 않습니다. 부모가 디바이스를 사용하기 전에 fork한 워커는 디바이스 매핑 변수를 설정한 뒤 독립적으로 디바이스를 사용할 수 있습니다. multiprocessing에서는 spawn 시작 방식을 권장하며 DataLoader 워커에서 rbln 텐서를 사용해서는 안 됩니다.

지연 할당 디바이스 텐서

rbln에 텐서를 만들어도 즉시 디바이스 메모리를 사용하지 않습니다. 런타임은 텐서의 가상 스토리지를 등록하고 할당 모드에 따라 물리 메모리를 확보합니다.

TORCH_RBLN_EAGER_MALLOC 모드 스토리지 등록 시 동작
미설정(기본값) 또는 0 지연 할당 가상 스토리지만 등록. 호스트 메모리와 NPU 메모리는 필요할 때 할당.
1 즉시 할당 가상 스토리지와 함께 NPU 메모리 할당.

애플리케이션에서는 기본값을 유지하는 것을 권장합니다. 즉시 할당 모드는 할당 동작의 실험과 프로파일링용입니다. 변수는 프로세스를 시작하기 전에 설정합니다.

지연 할당에서는 런타임이 스토리지마다 최신 데이터가 호스트 메모리와 NPU 메모리 중 어디에 있는지 추적하며 반대쪽에서 데이터를 필요로 할 때만 전송합니다.

작업 메모리와 데이터 이동
빈 rbln 텐서 생성 가상 스토리지를 등록. 호스트 메모리와 NPU 메모리는 아직 할당하지 않음.
CPU 데이터를 rbln 텐서에 복사(torch.tensor(...), .to("rbln"), fill_) 데이터를 호스트 메모리에 유지. NPU 연산이 사용할 때까지 NPU 전송을 미룸.
NPU 연산 실행 필요한 디바이스 메모리를 확보하고 최신 데이터가 호스트에 있는 입력을 NPU 메모리로 복사(H2D).
CPU에서 결과 읽기(.cpu(), .item()) 최신 데이터가 NPU에 있으면 호스트 메모리로 복사(D2H).
CPU fallback 호스트 복사본이 최신이면 사용. 그렇지 않으면 NPU의 데이터를 먼저 호스트 메모리로 복사.

이로 인해 다음 네 가지 동작이 나타납니다.

  • 메모리 통계는 텐서 생성 직후의 크기를 반영하지 않습니다. torch.rbln.memory_allocated()는 실제 확보한 디바이스 메모리를 세므로 할당 직후에는 작고 첫 디바이스 연산 이후 늘어납니다. 텐서 크기를 합산하기보다 대표 입력을 실행해 디바이스 메모리를 산정하는 것을 권장합니다. 메모리 부족 오류는 공간을 예약한 torch.empty가 아니라 텐서를 처음 사용하는 디바이스 연산에서 발생할 수 있습니다. 메모리 통계와 캐싱 할당기에서 확인할 수 있습니다.
  • 호스트 전용 워크로드는 NPU를 사용하지 않습니다. rbln 텐서를 만든 뒤 출력하거나 Python 정수로 인덱싱하거나 float32 산술만 수행하는 스크립트는 전부 호스트에서 실행됩니다. 텐서는 계속 device='rbln:0'으로 표시됩니다. 이런 테스트로는 디바이스 동작을 검증할 수 없습니다.
  • data_ptr()는 가상 주소입니다. CPU에서 역참조할 수 없습니다. 데이터는 .cpu()로 읽어야 합니다.
  • PyTorch 연산자를 거치지 않는 외부 라이브러리는 아직 할당되지 않은 NPU 물리 메모리에 접근할 수 없습니다. PyTorch 연산자를 거치지 않고 NPU 메모리에 직접 접근하는 외부 코드에는 물리 메모리가 확보된 버퍼를 전달해야 합니다. 지연 할당 때문에 텐서 생성만으로는 물리 메모리가 준비되지 않을 수 있으므로, 버퍼를 전달하기 전에 torch.rbln.bind_device_memory(tensor)를 호출해야 합니다. 이 함수는 전체 스토리지를 차지하는 연속 텐서(스토리지 오프셋 0)의 물리 메모리를 단일 평탄 영역으로 즉시 확보합니다.

    다른 텐서와 동일한 디바이스 메모리 레이아웃이 필요하면 torch.rbln.set_device_layout_like(target, ref)를 사용할 수 있습니다. 이 함수는 이후 디바이스 간 복사가 빠른 경로를 사용하도록 레이아웃을 맞추며, 데이터를 복사하지는 않습니다. 두 텐서는 같은 디바이스와 dtype을 사용하고 각각 전체 스토리지를 차지해야 하며, ref의 데이터는 디바이스에 있어야 합니다. 자세한 내용은 API와 지원 연산 › 메모리에서 확인할 수 있습니다.

메모리 통계와 캐싱 할당기

디바이스 메모리는 캐싱 할당기가 관리합니다. CUDA의 캐싱 할당기처럼, 사용이 끝난 메모리 블록을 보관했다가 이후 할당에 재사용합니다. 이 블록은 memory_reserved()에 포함되며, torch.rbln.empty_cache()를 호출하면 런타임에 반환됩니다. empty_cache()는 컴파일된 프로그램의 웜 캐시와 뷰 레시피 캐시도 제거합니다. 이로써 호출자가 더 이상 보유하지 않는 자원을 모두 해제합니다. 다음 eager 호출에는 준비 비용이 다시 듭니다. 성능이 중요한 루프에서는 반복 사이에 캐시를 비우지 않는 것을 권장합니다.

torch.rbln API 반환 정보
memory_allocated() 텐서가 현재 사용하는 디바이스 메모리
memory_reserved() 캐시된 빈 블록을 포함해 할당기가 보유한 디바이스 메모리
max_memory_allocated(), max_memory_reserved() 두 값의 최대 사용량
reset_peak_memory_stats() 현재 값에서 최대 사용량 추적을 다시 시작
memory_stats() allocated.current, allocated.peak, reserved.*, active.* 등 점으로 구분된 키를 포함한 전체 딕셔너리

메모리 통계는 모두 디바이스 메모리만 표시합니다. CUDA에서 예상하는 값보다 작게 보이는 데에는 두 가지 이유가 있습니다. 지연 할당 텐서는 디바이스 연산이 실제 메모리를 확보한 뒤부터 집계됩니다. 또한 Dynamo가 캐시한 컴파일 프로그램은 자체 디바이스 버퍼를 유지합니다. 이 버퍼는 활성 텐서의 사용량이 아닌 예약 메모리로 표시됩니다. 다음 예는 TORCH_RBLN_EAGER_MALLOC이 미설정인 상태에서 배치 직후와 실행 후의 통계를 읽습니다.

import torch

device = "rbln:0"
x = torch.randn(128, 128, dtype=torch.float16, device="cpu").to(device)
print("Allocated after placement:", torch.rbln.memory_allocated(device))
torch.rbln.reset_peak_memory_stats(device)

with torch.inference_mode():
    result = x + x
    host_result = result.cpu()

print("Allocated after execution:", torch.rbln.memory_allocated(device))
print("Reserved after execution:", torch.rbln.memory_reserved(device))
print("Peak allocated during execution:", torch.rbln.max_memory_allocated(device))
Allocated after placement: 0
Allocated after execution: 233472
Reserved after execution: 8388608
Peak allocated during execution: 233472

값은 SoC, 할당 단위, 캐시 상태에 따라 달라집니다. 디바이스 통계는 디바이스 레이아웃, 할당 단위, 런타임 버퍼를 반영하며 호스트 프로세스 메모리와 호스트에만 있는 텐서 데이터는 제외합니다. 텐서의 numel() * element_size() 합계와 디바이스 메모리 통계는 서로 다릅니다. 텐서 메모리만 측정하려면 먼저 실제 메모리를 확보하고 통계를 읽기 전에 torch._dynamo.reset()으로 Dynamo 캐시를 초기화해야 합니다. reset_peak_memory_stats()와 reset_accumulated_memory_stats()는 CUDA와 같은 방식으로 동작합니다.

호스트 메모리: pinned 버퍼와 대용량 버퍼

RBLN이 활성 가속기이면 pin_memory=True가 적용됩니다. pinned CPU 텐서는 페이지 정렬된 호스트 할당기에서 생성하며 가능한 범위에서 mlock을 수행합니다. pinned 메모리는 non_blocking=True 복사를 비동기로 실행하는 데 필요합니다(비차단 복사 참고).

대용량 호스트 버퍼용 API는 두 가지입니다. torch.rbln.huge_host_empty(nbytes)는 2 MiB 단위로 정렬되고 페이지가 미리 확보된 버퍼를 반환하며 디바이스가 스테이징 복사 없이 이 버퍼로 DMA를 수행할 수 있습니다. torch.rbln.offload()는 블록 안에서 할당한 텐서의 호스트 영역을 파일로 내보냅니다. 둘 다 API 및 지원 연산 › 메모리에서 설명합니다.

정밀도와 dtype

항목 CUDA RBLN
디바이스 연산 dtype 모든 dtype에 디바이스 커널 제공 float16과 bfloat16만 지원
float32 디바이스에서 실행. matmul에 TF32를 사용할 수 있음 호스트에서 실행(CPU fallback)
혼합 입력 디바이스에서 타입 승격 입력은 같은 디바이스 dtype이어야 함. float16과 bfloat16을 섞거나 비디바이스 dtype이 있으면 CPU로 fallback
정수와 bool 텐서 디바이스 커널 복사, 뷰, 인덱싱 같은 데이터 이동은 디바이스에서 수행. 산술은 호스트에서 수행하며 일반적인 정수 원소별 연산에는 빠른 호스트 경로 제공
양자화 가중치(W4A16, FP8, MXFP4) 라이브러리를 통해 양자화 커널 사용 eager 실행에는 양자화 dtype이나 커널이 없음. 정수와 float8 텐서를 rbln에 배치할 수 있으나 산술은 호스트에서 실행
Autocast(AMP) torch.autocast("cuda") 이번 릴리스에는 autocast dtype이 등록되지 않음. torch.rbln.get_amp_supported_dtype()는 빈 목록 반환
Scaled dot-product attention 여러 융합 백엔드 4차원 float16 / bfloat16 query, key, value에는 융합 디바이스 커널 사용. 다른 차원 수에서는 math 경로 사용
낮은 정밀도의 축약 연산 matmul별로 제어 가능 설정 불가. 디바이스 16비트 산술은 IEEE float16보다 가수 비트 수가 적은 런타임 자체 16비트 연산 형식을 사용하며 누적 정밀도는 컴파일러가 선택

모델과 입력을 rbln으로 옮기기 전에 float16 또는 bfloat16으로 변환해야 합니다. rbln에 배치한 float32 모델은 올바른 결과를 내지만 호스트에서 실행되며 explain()은 모든 연산자를 dtype-not-fp16 fallback으로 표시합니다.

양자화 모델은 RBLN vLLM으로 서빙합니다. RBLN vLLM은 W4A16, FP8, MXFP4 레이어를 가중치 역양자화 후 linear를 호출하는 일반 torch 코드로 표현합니다. RBLN Compiler는 torch.compile 그래프 안에서 이 패턴을 인식해 디바이스 커널로 매핑합니다. eager RBLN PyTorch에서 같은 레이어를 실행하면 융합되지 않습니다. 정수에서 half로 변환하는 작업은 호스트에서 실행되고 변환한 float16의 matmul만 디바이스에 도달합니다. RBLN vLLM 밖에서도 컴파일러가 이 패턴을 인식하는지는 이번 릴리스에서 검증하지 않았습니다.

포팅 결과를 검증할 때 고려할 사항은 다음 세 가지입니다.

  • rbln의 float16 결과는 CPU의 같은 연산 결과와 비트 단위로 일치하지 않습니다. 디바이스 연산 형식 때문에 모든 원소에 작은 반올림 차이가 생깁니다.
  • float16 범위의 경계에서는 단순한 반올림 차이를 넘어섭니다. subnormal 경계 근처의 값은 0으로 처리될 수 있고 최댓값 근처의 값은 재양자화됩니다. 이 범위의 div와 값이 비슷한 원소를 선택하는 max / min은 CPU와 결과가 다를 수 있습니다. 디바이스에서 무한대를 포함한 연산은 IEEE 754 표준과 다르게 동작할 수 있습니다. 예를 들어 inf - inf는 NaN 대신 유한한 값을 반환합니다. 기본 설정에서는 NaN/Inf 디스패치 전 검사가 이런 입력을 CPU로 보내므로 디바이스에 도달하지 않습니다. TORCH_RBLN_DEPLOY=ON으로 검사를 생략할 때 주의해야 합니다.
  • 허용 오차를 두고 float32 CPU 기준 결과와 비교하는 것을 권장합니다. 같은 dtype의 CPU 결과는 올바른 디바이스 실행 결과와도 다를 수 있으므로 기준으로 적합하지 않습니다. 자세한 내용은 문제 해결 › CPU 실행과 다른 결과에서 확인할 수 있습니다.

타입 승격은 디스패치 전에 검사합니다. 입력의 타입 승격 결과가 출력 dtype과 일치하고, 디바이스에서 지원하는 타입이면 디바이스에서 연산합니다. 그렇지 않으면 CPU로 fallback하고 explain() 보고서에 dtype-not-fp16을 이유로 표시합니다.

eager와 그래프 실행

모드 컴파일 단위 사용 방법
Eager 디바이스 연산 경로의 연산자 하나 rbln 텐서에 PyTorch 연산자 호출
Graph 함수나 모듈에서 캡처한 연산자 함수나 모듈을 torch.compile(backend="rbln")로 감싸기

연산자 디스패치 경로

rbln 텐서의 ATen 연산자는 PyTorch 디스패처가 RBLN PyTorch의 PrivateUse1 키로 보냅니다. 이후 동작은 연산자의 등록 방식에 따라 달라집니다.

등록 방식 연산자 실행 방식
컴파일 커널 API 및 지원 연산 › 컴파일 연산자의 연산자 연산자를 단일 연산자 모듈로 감싸고 torch.compile을 통해 RBLN Compiler가 현재 shape와 dtype에 맞춰 컴파일합니다. 프로그램을 캐시하며 이후 같은 입력 프로파일의 호출에서 재사용합니다.
네이티브 커널 복사, 뷰, cat, index_select 등 네이티브 연산자의 연산자 C++ 커널이 런타임을 직접 호출합니다. 컴파일하지 않습니다.
CPU 고정 fallback 난수 생성과 CPU에 고정된 연산자 입력을 호스트로 복사하고 CPU 커널을 실행한 뒤 출력을 다시 복사합니다.
미등록 나머지 연산자 CPU 고정 fallback과 같습니다. 다만 unsupported_op fallback이 비활성화되어 있으면 연산자 이름을 명시한 오류가 발생합니다.

컴파일 커널 경로에는 두 단계의 캐시가 있습니다. 연산자와 입력 프로파일 조합에서 처음 캐시 미스가 발생하면 프로그램을 빌드하며 수 초가 걸립니다. 이후 같은 프로파일의 디스패치는 Python이나 Dynamo에 진입하지 않고 런타임을 직접 호출하는 C++ 웜 캐시를 사용하므로 안정 상태에서 연산자별 eager 오버헤드는 작습니다. 캐시 키는 연산자 이름과 각 입력의 dtype, shape, stride, 스토리지 오프셋, 디바이스 인덱스, Python 스칼라로 전달한 인자의 값으로 구성합니다. 이 중 하나라도 달라지면 새 프로그램이 필요합니다.

재컴파일 없이도 매 호출마다 Python 디스패치 비용이 발생하는 경우가 있습니다. 이러한 호출은 C++ 웜 캐시를 사용하지 못하고 매번 Python 래퍼로 다시 진입합니다. 이때 Python 쪽 컴파일 캐시를 재사용하므로 컴파일 작업은 발생하지 않습니다.

  • 입력이 인식 가능한 뷰인 경우. 연속된 원본 텐서의 permute, expand, narrow, select, reshape가 이에 해당합니다. 컴파일된 래퍼 내부에서 뷰 체인을 재현하며 뷰 shape로 만든 C++ 키가 원본 shape로 컴파일한 프로그램과 일치하지 않습니다.
  • 이번 릴리스에서는 Python 스칼라를 피연산자로 사용하는 연산(예: x + 1.0)과 같은 텐서를 두 피연산자로 사용해 처음 컴파일한 연산(예: x + x)도 C++ 웜 캐시를 사용하지 못합니다. 같은 입력 프로파일의 서로 다른 텐서 두 개로 먼저 컴파일하면(예: 같은 shape·dtype의 x와 y를 사용한 x + y), 이후 x + x 호출에서도 C++ 웜 캐시 항목을 재사용할 수 있습니다.

컴파일 단위가 연산자 하나이므로 입력 shape가 호출마다 바뀌면 그래프별 하나가 아니라 연산자별, shape별 프로그램이 필요합니다. 두 단계의 캐시는 모두 생성한 프로세스 안에 존재합니다. RBLN PyTorch는 torch._dynamo.config.specialize_float = True도 설정하므로 Python float 인자가 호출 사이에 바뀌어도 새 프로그램이 필요합니다. 모델을 포팅하기 전에 패딩이나 버킷팅으로 입력 shape를 정해진 집합으로 제한하는 것을 권장합니다. 자세한 내용은 문제 해결 › 재컴파일 한도 초과에서 확인할 수 있습니다.

디스패치 전 검사

컴파일 커널 연산자를 디바이스로 보내기 전에 다음 검사를 순서대로 수행해 fallback 여부를 결정합니다.

검사 fallback 조건 explain() 출력 보충 설명
트레이서 Python trace 함수가 활성 상태(pdb, coverage, sys.settrace) — 트레이서 아래에서 컴파일을 실행하면 안 됨
디스패치 모드 인프라용이 아닌 TorchDispatchMode가 활성 상태 — 컴파일 중 재귀 발생
Dtype 입력 dtype이 float16 / bfloat16 밖에 있거나 두 디바이스 dtype을 혼합 dtype-not-fp16 —
스칼라 모든 입력이 0차원 텐서 all-scalar inputs —
스토리지 오프셋 연속 입력의 storage_offset != 0 — —
NaN / Inf TORCH_RBLN_DEPLOY=ON이 아닌 상태에서 입력에 NaN 또는 Inf가 있음 nan/inf input —
재진입 다른 RBLN 컴파일 연산자 안에서 디스패치 발생(print, repr, 중첩 연산자) — 경고 기록
마지막 차원 정렬 텐서 인자의 마지막 차원이 64의 배수가 아님. 텐서 목록 인자(cat, stack)는 예외로 디바이스에서 실행 — —
div 반올림 모드 디바이스 dtype에서 torch.div의 rounding_mode="trunc" 또는 "floor" 사용 — —

dtype, 스칼라, NaN/Inf 검사는 C++ shim에서 결정하며 앞의 why: 이유를 표시합니다. 나머지 검사는 shim에서 캐시 미스가 난 뒤 Python 래퍼에서 수행합니다. 이 조건에 걸리는 호출은 매번 웜 캐시를 사용하지 못하며 explain() 보고서에서 dispatch/cpu_fallback 카운터 대신 반복되는 dispatch/recompile로 나타납니다. fallback 로그에는 연산자 이름이 표시되고 디바이스에 있는 각 입력은 CPU 커널 실행을 위해 다운로드합니다. NaN/Inf 검사도 호스트에서 입력을 읽으므로 최신 데이터가 디바이스에 있으면 다운로드 비용이 듭니다. TORCH_RBLN_DEPLOY=ON은 입력이 유효하다는 전제로 이 검사를 생략합니다.

개발할 때만 TORCH_RBLN_DEV_DISABLE_OP_CPU_FALLBACK으로 각 검사를 개별적으로 생략할 수 있습니다. 이 변수는 환경 변수 › 개발 전용 옵션에 설명되어 있습니다.

그래프 실행과 입력 shape

torch.compile(model, backend="rbln")은 연산자마다 프로그램을 만드는 대신 캡처한 전체 그래프를 하나의 프로그램으로 컴파일합니다. 백엔드는 RBLN Compiler의 일부이며 옵션은 파이썬 API › 파이토치 컴파일 API에서, 모델 예제는 PyTorch ResNet50 › torch.compile() API 사용에서 설명합니다. 여기서는 RBLN PyTorch가 추가하는 디바이스 상주 입출력, 실패 시 fallback, shape 규칙을 다룹니다. rbln 백엔드는 첫 torch.compile 호출에 등록되므로 import torch 외에 추가 import가 필요하지 않습니다.

RBLN Compiler는 항상 고정된 입력 shape로 컴파일하므로 dynamic=False를 지정해야 합니다. dynamic=True에서 텐서 크기를 읽는 코드(x.shape[0], view)는 두 번째 shape를 처리할 때 Dynamic shape not supported for rbln backend 오류가 발생합니다. 기본 fallback 설정에서는 compile_error fallback이 이 오류를 처리하고 전체 그래프를 CPU에서 실행합니다.

항목 rbln eager 실행 torch.compile(backend="rbln")
컴파일 단위 연산자 하나 캡처한 그래프
연산자 지원 범위 컴파일 및 네이티브 연산자 표 RBLN Compiler가 결정. 지원하는 연산 - 파이토치 참고
미지원 연산자 연산자별 CPU fallback 컴파일 오류. compile_error fallback이 비활성화되어 있지 않으면 원래 함수를 CPU에서 실행
디바이스 수 num_devices=1 options={"num_devices": N}으로 재정의하지 않으면 입력의 논리 디바이스 NPU 수. 텐서 병렬 실행 참고
동적 shape 새 shape마다 연산자별 프로그램 컴파일 새 shape마다 그래프 재컴파일. Dynamo 재컴파일 한도 적용
입력 dtype과 shape 제약 디스패치 전에 연산자별로 검사 컴파일러가 처리. 변환할 수 없는 연산자는 결과물 내부의 호스트 연산자로 분리
import torch


def compute(x, y):
    return torch.relu(x + y)


x = torch.randn(128, 128, dtype=torch.float16, device="rbln:0")
y = torch.randn_like(x)
compiled_compute = torch.compile(compute, backend="rbln", dynamic=False)

with torch.inference_mode():
    first_result = compiled_compute(x, y)  # 이 입력 shape로 컴파일
    next_result = compiled_compute(x, y)  # 같은 입력 프로파일: 프로그램 재실행

    smaller_x = torch.randn(64, 128, dtype=x.dtype, device=x.device)
    smaller_y = torch.randn_like(smaller_x)
    smaller_result = compiled_compute(smaller_x, smaller_y)  # 새 shape: 다시 컴파일

처음 두 호출은 입력 프로파일이 같아 프로그램을 재사용합니다. 세 번째 호출은 shape를 (128, 128)에서 (64, 128)로 바꾸므로 일치하는 캐시 항목이 없으면 그래프를 다시 컴파일합니다. Rebel100™ 카드에서 연산자 두 개로 구성된 단계를 측정했을 때 첫 호출은 약 800 ms, 재실행은 0.2 ms, 새 shape는 약 400 ms가 걸렸습니다. TORCH_RBLN_DISABLE_FALLBACK=compile_error를 설정하면 컴파일에 실패할 때 전체 함수를 CPU에서 실행하는 대신 오류가 발생합니다. torch.compile을 사용하는 코드를 개발할 때는 이 환경 변수를 설정하는 것을 권장합니다. 입력 크기가 바뀌는 서비스에서는 배치 크기와 시퀀스 길이를 고정하고 패딩이나 버킷팅으로 입력을 맞추면 런타임 shape 변경에 따른 컴파일을 피할 수 있습니다.

options는 RBLN Compiler에 그대로 전달됩니다. num_devices는 RSD 그룹 크기를 재정의하고 cache_dir은 결과물 저장 위치를, mode는 컴파일 전용 빌드를 선택합니다. npu는 대상 SoC를 지정합니다. torch._dynamo.reset()은 RBLN PyTorch의 컴파일 캐시를 비우고 디바이스 버퍼를 해제하기 전에 진행 중인 작업을 완료합니다.

모델이 작은 연산자 여러 개로 구성되거나, eager 지원 표 밖의 연산자가 자주 실행되는 경로에 있거나, 프로그램을 RSD 그룹에 분할해야 한다면 torch.compile을 선택하는 것을 권장합니다. eager는 개발과 디버깅, 컴파일 영역 주변의 연결 코드에 사용합니다. 서빙 단계에서 두 방식을 함께 사용하는 방법은 전체 단계의 디바이스 실행에서 설명합니다.

뷰와 strided 복사

뷰 연산자(view, as_strided, permute, narrow, expand, select, squeeze, unsqueeze)는 CUDA와 마찬가지로 RBLN에서도 메타데이터만 다룹니다. 원본과 스토리지를 공유하며 데이터를 이동하지 않습니다. 뷰를 입력으로 사용할 때 차이가 나타납니다.

  • 컴파일 커널 연산자가 연속 원본의 인식 가능한 뷰를 받으면 뷰를 원본으로 바꾸고 컴파일된 래퍼 안에서 뷰 체인을 재현하므로 연산은 디바이스에서 실행됩니다. 비교 연산자(eq, ne, lt, le, gt, ge)는 예외로, 뷰 입력을 먼저 .contiguous()로 구체화합니다.
  • base[1:, :]처럼 스토리지 오프셋이 0이 아닌 연속 뷰는 스토리지 오프셋 검사에 걸려 CPU에서 실행됩니다.

    1
    2
    3
    4
    5
    6
    7
    8
    import torch
    
    base = torch.randn(128, 128, dtype=torch.float16, device="rbln:0")
    part = base[1:, :]
    print(part.is_contiguous(), part.storage_offset())  # 기대 출력: True 128
    
    with torch.inference_mode():
        result = part + part  # storage_offset != 0이므로 CPU로 fallback합니다.
    

    뷰 생성과 덧셈 실행은 별개이며 덧셈만 fallback합니다.

  • strided 뷰에서 수행하는 디바이스 간 copy_는 호스트를 왕복하지 않고 디바이스에서 처리합니다.

    런타임은 strided 디바이스 복사를 직접 실행하거나, 뷰가 연속 원본에서 시작하는 인식 가능한 단일 단계 뷰 연산의 체인이면 작은 컴파일 그래프로 체인을 재현해 컴파일러가 디바이스에서 실행하도록 변환합니다.

  • 런타임이 분류할 수 없는 뷰 체인은 호스트를 거쳐 .contiguous()로 구체화하며 프로세스당 한 번 경고를 표시합니다. explain()에는 host_bounce 신호로 표시됩니다.

  • out=으로 지정한 출력 텐서가 비연속이면, 런타임은 임시 연속 버퍼에 결과를 저장한 뒤 출력 텐서로 복사합니다. 이 과정에서 메모리 할당과 복사가 각각 한 번 추가됩니다. 자주 실행되는 코드에서는 out=에 연속 출력 텐서를 지정하는 것을 권장합니다.

CPU fallback

CPU fallback은 디바이스가 처리하지 못하는 연산자나 입력을 CPU에서 실행하고 결과를 rbln 텐서로 반환하는 경로입니다. 결과의 디바이스는 배치를 나타냅니다. 실제 연산 위치는 explain()이나 fallback 로그로 확인할 수 있습니다(실행 경로 검증 참고).

fallback이 발생하는 경우

경로 조건 동작
일반 백엔드 fallback RBLN에 전혀 등록되지 않은 연산자 입력의 호스트 복사본에 CPU 커널 실행
eager 연산 경로 디스패치 전 검사 중 하나에 실패 입력의 호스트 복사본에 CPU 커널 실행
컴파일 함수 래퍼 torch.compile(backend="rbln") 호출에 예외가 발생하고 compile_error fallback을 허용 호출 인자의 CPU 복사본으로 원래 Python 함수 실행

자체 디바이스 구현이 있는 연산자는 지원 연산의 조건을 따릅니다. CPU도 지원하지 않는 연산자나 잘못된 입력은 예외가 발생합니다.

컴파일 함수 fallback은 호출 인자를 CPU로 변환하지만 모듈 매개변수나 함수가 인자 외부에서 읽는 텐서는 변환하지 않습니다. 이 텐서가 rbln에 남아 있으면 CPU 실행에서 디바이스 불일치 오류가 발생합니다. 이 fallback에서는 전체 그래프를 호스트에서 실행하므로 단계가 디바이스에 머무는지 확인할 때는 compile_error 범주를 비활성화해 컴파일 실패를 오류로 드러내야 합니다.

fallback 비용

fallback은 모든 텐서 인자를 호스트로 옮기고 CPU 커널을 실행한 뒤 출력을 다시 디바이스로 옮깁니다. 최신 데이터가 여전히 호스트 뷰에 있는 지연 할당 텐서는 디바이스에서 호스트로 옮기는 비용이 없습니다. 실제 디바이스 메모리가 있는 텐서는 동기화 후 DMA를 수행합니다. explain() 보고서의 >> physical d2h 행으로 두 경우를 구분할 수 있으므로 fallback 횟수와 전송량을 함께 확인하는 것을 권합니다. 디바이스 커널이 없는 연산자의 fallback과 Python 디스패치 계층이 CPU로 보내는 연산자의 fallback은 INFO 수준으로 연산자 이름과, 추적 가능한 경우 소스 위치를 기록합니다. 디스패치 전 검사가 결정한 fallback은 explain()에서만 확인할 수 있습니다. 문제 해결 › CPU fallback 로그에서 확인할 수 있습니다.

자주 fallback하는 연산자에는 일반 boxed 디스패처를 우회하는 빠른 호스트 경로가 있습니다. 해당 연산자는 CPU에 고정된 연산자에 정리되어 있습니다. 이런 경로가 없는 연산자는 explain() 출력에 최적화 후보로 표시됩니다.

fallback 제어

미지원 연산자나 컴파일 실패를 CPU 실행 대신 오류로 드러내려면 프로세스를 시작하기 전에 TORCH_RBLN_DISABLE_FALLBACK에 범주를 지정합니다.

TORCH_RBLN_DISABLE_FALLBACK=compile_error,unsupported_op python application.py

각 범주는 해당 경로만 차단합니다. compile_error는 컴파일 함수 래퍼의 fallback을, unsupported_op는 RBLN에 등록되지 않은 연산자의 일반 fallback을, strided_copy_error는 디바이스 strided 복사 엔진 실패 시 fallback을 차단합니다. dtype, 스칼라, NaN/Inf 등 앞 표의 디스패치 전 검사는 범주가 아니므로 영향을 받지 않습니다. CPU fallback 경로에 명시적으로 등록된 연산자(CPU에 고정된 연산자)는 설계상 항상 호스트에서 실행됩니다. 범주 표, 기본 동작, 범주를 읽는 시점은 환경 변수 › 실행과 fallback에서 확인할 수 있습니다.

텐서 병렬 실행

이 절은 ATOM™에서 RSD 그룹과 컴파일 옵션으로 설정하는 텐서 병렬(TP) 실행을 설명합니다. Rebel100™의 RSD 그룹은 카드의 칩렛 네 개로 고정되므로 이 설정은 적용되지 않습니다. 여러 Rebel100™ 카드는 rbln-ccl로 연결합니다. RSD 그룹과 torch.distributed의 프로세스 그룹 및 world size는 별도로 설정합니다. 분산 실행에서 확인할 수 있습니다.

실행별 NPU 수

ATOM™에서 연산자가 요청하는 NPU 수는 다음 규칙을 따릅니다. 여러 NPU에서 실행하려면 컴파일러가 연산자를 분할할 수 있어야 합니다.

  • eager 연산 경로: 1입니다.
  • 그래프 실행: 기본값은 입력 텐서가 있는 논리 디바이스의 NPU 수입니다. torch.compile의 options={"num_devices": N}으로 N을 지정할 수 있습니다.

예를 들어 rbln:0이 ATOM™ NPU 네 개를 묶고 입력이 rbln:0에 있으면 다음과 같습니다.

모드 설정 요청 NPU 수
eager 연산 기본값 1
Graph 기본값(num_devices 생략) 4
Graph options={"num_devices": 1} 1

분산 실행

텐서 병렬 실행은 프로세스 하나 안에서 하나의 논리 디바이스에 속한 NPU에 프로그램을 분할합니다. 분산 실행은 각자 논리 디바이스를 사용하는 여러 프로세스가 집합 통신으로 텐서를 주고받는 방식입니다. RBLN PyTorch는 rbln과 cpu 디바이스 타입에 rbln-ccl이라는 torch.distributed 백엔드를 등록합니다. 실제 하드웨어가 필요합니다.

import os

import torch
import torch.distributed as dist

# torchrun으로 실행합니다. 디바이스를 처음 사용하기 전에 rank마다 사용할 카드를 지정합니다.
cards = os.environ["RBLN_VISIBLE_DEVICES"].split(",")
os.environ["RBLN_VISIBLE_DEVICES"] = cards[int(os.environ["LOCAL_RANK"])]

dist.init_process_group(backend="rbln-ccl")
t = torch.ones(1024, dtype=torch.float16, device="rbln")
dist.all_reduce(t)

카드당 프로세스 하나로 실행합니다. 예: RBLN_VISIBLE_DEVICES=0,1 torchrun --nproc_per_node 2 <SCRIPT>.py. 전체 스크립트와 출력은 튜토리얼: rbln-ccl 집합 통신에서 확인할 수 있습니다.

항목 동작
집합 통신 broadcast, scatter, allgather(_allgather_base 및 coalesced 포함), allreduce, reduce_scatter(_reduce_scatter_base 포함), send, recv, barrier
Reduce 연산자 집합 통신 라이브러리가 지원하는 SUM, AVG, MIN, MAX, PRODUCT. 비트 연산자와 PREMUL_SUM은 예외 발생
디바이스 dtype float16 집합 통신은 NPU에서 실행. 다른 dtype의 allreduce와 reduce_scatter는 Gloo가 있으면 Gloo 프로세스 그룹이 호스트 복사본에서 실행하고 없으면 예외 발생.
기본 동기 실행 집합 통신은 완료 후 반환. TORCH_RBLN_C10D_ASYNC=1이면 완료를 기다릴 작업 핸들 반환.
제어 평면 rank 정보는 init_process_group에 전달한 store를 사용. RBLN_ROOT_IP와 RBLN_LOCAL_IP의 기본값은 단일 호스트에서만 사용할 수 있는 127.0.0.1. 여러 노드에서는 설정 필요. 환경 변수 › 분산 실행 참고.

동기화, 스트림, 이벤트

RBLN PyTorch는 복사와 컴파일 그래프 실행을 런타임의 현재 스트림에 제출합니다. 스트림과 이벤트는 작업 순서를 정하고 호스트가 완료를 기다릴 수 있게 합니다.

비차단 복사

복사 CUDA RBLN
tensor.to("rbln") / .to("cpu") non_blocking=True가 아니면 호스트와 동기 실행 동일. 동기 경로는 시작 전에 진행 중인 비동기 전송을 완료함.
pinned 호스트 메모리의 non_blocking=True 비동기. 호출자가 동기화 다음 조건을 충족하면 런타임 DMA 큐로 비동기 실행. 호출자는 torch.rbln.synchronize() 또는 이벤트로 대기
pageable 호스트 메모리의 non_blocking=True 사실상 동기 실행 동기 복사로 전환
작은 복사 여러 개 복사마다 실행 요청 _foreach_copy_가 방향별 복사를 묶어 런타임에 한 번에 제출

non_blocking=True이면 전송이 진행 중이어도 Python 호출이 반환될 수 있습니다. Tensor.to()와 Tensor.copy_()는 다음 조건을 모두 충족할 때만 비동기 경로를 사용합니다.

  • 원본과 대상의 shape와 dtype이 같습니다. dtype 변환이나 브로드캐스트가 필요한 복사는 동기 방식입니다.
  • 두 텐서 모두 연속 텐서입니다.
  • 호스트-디바이스 복사에서는 호스트 버퍼가 RBLN pinned 할당기에서 생성되어야 합니다(pin_memory=True 또는 Tensor.pin_memory()).
  • 호스트에서 디바이스로 복사할 때는 대상이 이미 단일 평탄 영역의 디바이스 메모리에 있어야 합니다.

새로 만든 지연 할당 텐서는 아직 디바이스 메모리가 없으므로 마지막 조건을 충족하지 못합니다. 복사는 호스트 메모리에 도착하고 NPU 연산이 텐서를 사용할 때 업로드합니다. torch.rbln.bind_device_memory()는 전체 스토리지를 차지하는 텐서(연속 텐서, 스토리지 오프셋 0)에 평탄 영역을 미리 할당합니다. pinned 호스트 텐서에서 이 버퍼로 non_blocking=True 복사를 하면 비동기 전송을 시작합니다.

이번 릴리스에서는 버퍼를 eager 또는 컴파일된 NPU 연산의 입력으로 한 번 사용하면 런타임이 해당 연산의 입력 레이아웃에 맞춰 버퍼를 재배치합니다. 이후 같은 버퍼로 수행하는 non_blocking=True 복사는 동기 방식으로 처리됩니다. 비동기 업로드는 버퍼를 처음 사용하기 전에만 확인할 수 있습니다. 반복 단계의 입력 버퍼에서는 한 번의 동기 호스트-디바이스 복사로 처리되며, 이 복사는 복사 호출 안에서 완료됩니다. explain()은 동기 업로드를 host->device push로 집계하며 비동기 경로의 전송은 집계하지 않습니다.

조건을 충족하지 못하는 복사는 동기 방식입니다. 런타임은 경로를 바꾸기 전에 대기 중인 비동기 전송을 완료하며 전환 자체는 오류를 발생시키지 않습니다. RBLN 논리 디바이스 간 복사는 런타임의 디바이스 간 복사가 처리합니다. 호스트 메모리를 거칠 수 있으며 explain()에서 집계하지 않습니다.

비동기 전송 중에는 완료될 때까지 두 버퍼를 유지해야 합니다. 완료 전에 원본을 수정하거나 대상을 재사용해서는 안 되며 CPU에서 대상을 읽기 전에 완료를 기다려야 합니다.

1
2
3
4
5
6
7
8
import torch

host = torch.empty(1 << 20, dtype=torch.float16, pin_memory=True)
dev = torch.empty(1 << 20, dtype=torch.float16, device="rbln")
torch.rbln.bind_device_memory(dev)  # 대상 텐서의 디바이스 메모리를 미리 할당합니다.

dev.copy_(host, non_blocking=True)  # 전송이 끝나기 전에 반환합니다.
torch.rbln.synchronize()  # 현재 디바이스에서 대기 중인 모든 전송과 스트림이 완료될 때까지 기다립니다.

스트림

torch.rbln.Stream과 torch.rbln.Event는 torch.cuda.Stream, torch.cuda.Event에 대응하며 범용 torch.Stream(device="rbln")과 torch.Event(device="rbln")도 사용할 수 있습니다. 하나의 스트림에 제출한 작업은 제출 순서대로 실행됩니다. 완료는 스트림별로 추적하므로 다른 스트림에 작업이 남아 있어도 한 스트림의 query()는 True를 반환할 수 있습니다. 스트림 간 작업 순서를 정하려면 이벤트를 기록하고 소비하는 스트림에서 기다려야 합니다. 같은 디바이스에서 이 대기는 디바이스 측 fence로 처리하며 호스트를 차단하지 않습니다.

항목 CUDA RBLN
스트림 풀 필요할 때 생성 디바이스당 스트림 32개의 고정 풀을 라운드 로빈으로 사용. 풀 크기를 넘으면 새 인스턴스가 기존 스트림을 재사용하므로 Python 스트림 객체 두 개가 같은 큐를 가리킬 수 있음
현재 스트림 스레드별 스레드별. 한 스레드의 선택은 다른 스레드에 영향을 주지 않음. 선택하지 않은 스레드는 기본 스트림에 제출
우선순위 지원 API 호환성을 위해 허용하지만 무시. priority_range()는 (0, 0) 반환
기본 스트림 스트림 0 디바이스마다 스트림 ID 0
컴파일 그래프의 스트림 실행 요청 시 선택 그래프 실행 시작 시 한 번 선택. 실행 중 현재 스트림을 바꿔도 이미 시작한 그래프에는 영향 없음. 그래프 내부 순서는 컴파일 시 고정하며 스트림은 그래프 실행과 복사 사이의 순서를 제어
실행 중첩 서로 다른 스트림의 커널을 겹쳐 실행할 수 있음 런타임에 제출한 전송과 실행은 호스트 작업과 겹칠 수 있음. 이번 릴리스에서는 여러 스트림으로 나눠도 디바이스 병렬성이 추가되지 않음
eager 연산자 연산자마다 실행 요청 각 연산자가 자체 컴파일 프로그램을 제출하므로 호출마다 호스트 디스패치와 제출 비용 발생. 프로파일링할 때 디바이스 실행 시간과 구분
외부 공유 cudaStream_t 핸들 torch.Stream.native_handle()과 프로세스 간 스트림 공유 미지원
import torch

x = torch.ones(1024, dtype=torch.float16, device="rbln")

s = torch.rbln.Stream()
with torch.rbln.stream(s):
    y = x * 2  # s가 현재 스트림인 동안 작업을 제출합니다.
e = torch.rbln.Event()
e.record(s)
torch.rbln.current_stream().wait_event(e)

이벤트와 대기

이벤트는 스트림의 제출 시점을 표시합니다. 기록 이후 같은 스트림에 제출한 작업은 이벤트 범위에 포함되지 않습니다.

이벤트 API RBLN 동작
record(stream=None) 현재 또는 지정한 스트림의 가장 최근 제출 시점을 기록합니다. 이벤트와 스트림은 같은 디바이스에 있어야 합니다.
synchronize() 기록한 시점까지의 작업이 완료될 때까지 호스트가 기다립니다.
wait(stream) 같은 디바이스에서는 디바이스 측 fence를 설정해 stream의 후속 작업이 기록 시점까지 기다리도록 하며 호스트는 차단하지 않습니다. 디바이스가 다르면 호스트 측 대기로 전환합니다.
query() 차단 없이 완료 여부를 반환하므로 폴링에 사용할 수 있습니다. 기록하지 않은 이벤트는 True를 반환합니다.
elapsed_time() 미지원. enable_timing, blocking, interprocess 플래그는 허용하지만 무시합니다. 호스트 타이머나 RBLN Profiler를 사용할 수 있습니다.
ipc_handle(), from_ipc_handle() 미지원. NotImplementedError가 발생하며 interprocess=True를 지정해도 기능을 제공하지 않습니다.

torch.rbln.synchronize(device)는 논리 디바이스의 대기 중인 비동기 전송과 모든 스트림의 작업을 기다립니다. 인자가 없으면 현재 디바이스를 사용합니다. 특정 제출 시점까지 기다릴 때는 이벤트를, 디바이스 전체를 기다릴 때는 synchronize()를 사용합니다.

import torch

with torch.rbln.device(0):
    device_buf = torch.empty(1 << 24, dtype=torch.float16, device="rbln")
    torch.rbln.bind_device_memory(device_buf)
    host_src = torch.ones_like(device_buf, device="cpu", pin_memory=True)
    done = torch.Event(device="rbln")

    device_buf.copy_(host_src, non_blocking=True)
    done.record()
    ready = done.query()
    print("query() right after record():", ready)
    if not ready:
        done.synchronize()  # 전송이 진행 중이므로 완료될 때까지 기다립니다.

    host_src.zero_()  # 전송이 끝났으므로 원본 버퍼를 다시 사용할 수 있습니다.
query() right after record(): False

기록 직후 False이면 복사 호출이 반환될 때 비동기 전송이 진행 중이었다는 뜻입니다. True이면 이미 완료된 상태입니다. 이 값만으로는 동기 복사와 빠르게 완료된 비동기 복사를 구분할 수 없습니다. 전송 크기에 따라 복사 호출의 반환 시간이 늘어난다면 동기 경로를 사용하고 있는 것입니다. 폴링 중에도 앞의 버퍼 규칙을 지켜야 합니다.

시간 측정

elapsed_time() 대신 호스트 타이머로 영역을 측정합니다. 먼저 워밍업해 컴파일과 초기화 비용을 제외하고 타이머를 시작하기 전에 동기화하며 측정 대상 작업이 완료된 뒤 타이머를 멈춥니다. 다음 예는 CPU에서 결과를 사용할 수 있을 때까지의 시간을 측정합니다.

import time
import torch

x = torch.randn(128, 128, dtype=torch.float16, device="rbln:0")
y = torch.randn_like(x)

with torch.inference_mode():
    for _ in range(3):
        (x + y).cpu()  # 워밍업: 컴파일한 뒤 결과를 호스트로 가져옵니다.

    torch.rbln.synchronize(0)
    start = time.perf_counter()
    result = (x + y).cpu()
    elapsed_seconds = time.perf_counter() - start

print(elapsed_seconds)

측정에는 디스패치, 실행, 결과 다운로드가 포함됩니다. 예상보다 오래 걸리면 torch.rbln.explain() 보고서의 host->device push와 physical d2h 행에서 동기 복사와 호스트 왕복 전송을 확인합니다. NPU 내부에서 사용한 시간은 RBLN Profiler로 분석합니다.

전체 단계의 디바이스 실행

RBLN의 이상적인 단계 구성은 시작할 때 입력을 한 번 업로드(H2D)하고 끝날 때 결과를 한 번 다운로드(D2H)하며 그 사이의 모든 연산자와 중간 결과를 디바이스에 유지하는 것입니다. PyTorch 코드만으로는 이를 보장하지 않습니다. 앞의 규칙 때문에 평범해 보이는 코드 한 줄이 호스트 왕복 전송이나 CPU 실행을 일으킬 수 있고 비용은 매 단계 반복됩니다. 이 절에서는 호스트를 거치게 만드는 패턴과 이를 피하는 작성 방식을 설명합니다.

텐서 데이터의 세 가지 상태

런타임은 스토리지마다 최신 데이터가 호스트 메모리와 NPU 메모리 중 어디에 있는지 추적합니다. 이 문서에서는 상태를 다음과 같이 부릅니다.

상태 의미 진입 조건
Host-latest 최신 데이터가 호스트 메모리에만 있음 디바이스 메모리를 미리 확보하지 않은 텐서에 CPU 데이터를 복사하거나 device="rbln"로 텐서를 생성(torch.ones(..., device="rbln"))
Device-latest 최신 데이터가 NPU 메모리에만 있음 NPU 연산이 결과를 기록
In sync 양쪽에 같은 데이터가 있음 NPU 연산이 host-latest 데이터를 사용(H2D 발생)하거나 CPU가 device-latest 데이터를 읽음(D2H 발생)

전송은 상태가 바뀔 때만 발생하므로 목표는 다음과 같습니다.

  • 입력은 단계 시작 시 한 번만 host-latest → in sync로 바뀝니다.
  • 중간 결과는 device-latest 상태로 다음 연산자에 전달하며 호스트에서 읽지 않습니다.
  • 결과만 단계 끝에 한 번 device-latest → in sync로 바뀝니다.

단계 중간에 device-latest 텐서를 호스트에서 읽는 코드 한 줄이 D2H와 동기 대기를 추가합니다. 매 단계 만드는 host-latest 텐서는 H2D를 반복합니다.

호스트를 거치는 코드

패턴 동작 explain() 신호 권장 방식
.item(), float(t), bool(t), if t > 0:, print(t), .tolist(), .numpy() device-latest 데이터를 다운로드하고 완료를 기다림 >> physical d2h 값에 따른 분기는 디바이스 연산(torch.where, clamp)으로 표현. 로깅과 검사는 단계 끝으로 이동
단계 내부의 torch.tensor(...), torch.ones(..., device="rbln"), .to("rbln") device="rbln"이어도 데이터는 호스트에서 만들어져 host-latest가 됨. 첫 NPU 연산자가 업로드 host->device push 상수와 가중치는 단계 밖에서 한 번 생성. 미리 할당한 입력 버퍼를 copy_로 채워 단계당 H2D 한 번만 수행
Python 스칼라 피연산자(x + 1.0, h * 0.5) 전송은 없지만 eager 웜 캐시를 사용하지 못해 매번 Python 디스패치로 재진입 매 호출의 dispatch/recompile 스칼라를 텐서 상수로 한 번 만들고(torch.full_like) 재사용
float16/bfloat16이 아닌 dtype, 혼합 dtype, 모든 입력이 스칼라인 경우 CPU fallback. device-latest 입력을 먼저 다운로드하며 host-latest인 결과는 다음 NPU 연산자가 업로드 dispatch/cpu_fallback 인덱스와 카운터 산술은 CPU 텐서에서 수행하거나 torch.compile 내부로 이동. 컴파일러는 그래프의 int32 입력을 디바이스에서 처리
단계 내부의 dtype 변환(.to(torch.float32), .float(), 다른 dtype 텐서로 copy_) 호스트에서 변환하며 D2H와 H2D 모두 발생 host_bounce/d2d_copy 단계 안에서는 dtype 하나를 사용하고 변환은 밖에서 수행
마지막 차원이 64의 배수가 아닌 텐서의 eager 연산자 Python 래퍼에서 CPU fallback. dispatch/cpu_fallback에 집계되지 않고 반복되는 dispatch/recompile로 표시. 각 device-latest 입력을 다운로드 매 호출의 dispatch/recompile, >> physical d2h 마지막 차원을 64의 배수로 패딩하거나 연산자를 torch.compile 내부에 배치
스토리지 오프셋이 0이 아닌 뷰(x[1:]), 비연속 뷰(x.t()) 같은 래퍼 수준 fallback 또는 호스트를 거치는 레이아웃 변환 반복되는 dispatch/recompile, host_bounce 오프셋이 0인 연속 텐서를 입력으로 사용. 일부 결과가 필요하면 별도 out= 버퍼 제공
비연속 대상이나 다른 dtype으로 copy_ 호스트를 거쳐 복사 host_bounce/d2d_copy 같은 dtype과 shape의 연속 텐서 간 복사. 오프셋이 0인 연속 앞부분은 디바이스에서 복사
다른 논리 디바이스로 복사 런타임의 디바이스 간 복사. 호스트를 거칠 수 있음 집계하지 않음 한 단계의 텐서를 하나의 논리 디바이스에 유지
eager 입력의 NaN/Inf 검사(TORCH_RBLN_DEPLOY가 ON이 아닐 때) device-latest 텐서를 사용하는 모든 eager 연산자가 검사하려고 전체 텐서를 다운로드. 중간 결과가 디바이스에 머무를 수 없음 연산자마다 입력 크기의 >> physical d2h eager 연산자로 단계를 구성하려면 TORCH_RBLN_DEPLOY=ON 설정. 이 검사만 생략하며 입력의 유효성은 사용자가 보장

마지막 행은 eager 실행에만 적용됩니다. 컴파일된 그래프의 입출력은 검사하지 않습니다. 비용은 직접 확인할 수 있습니다. device-latest인 z를 사용하는 eager 연산 z * y는 TORCH_RBLN_DEPLOY가 미설정이면 z 전체 크기의 D2H를 한 번 수행하고 TORCH_RBLN_DEPLOY=ON이면 수행하지 않습니다.

eager와 그래프의 경계

컴파일된 그래프는 단계를 디바이스에 유지하는 가장 확실한 방법입니다. 그래프 경계에서는 다음과 같이 동작합니다.

  • device-latest 또는 in sync 상태의 그래프 입력은 전송 비용이 없습니다. host-latest 입력은 호출마다 업로드합니다.
  • 그래프 출력을 다음 그래프의 입력으로 전달할 때는 전송 비용이 없습니다.
  • 그래프 출력을 사용하는 eager 연산자는 앞의 NaN/Inf 규칙을 따릅니다.
  • 그래프 내부의 dtype과 shape 제약은 컴파일러가 처리합니다. 변환할 수 없는 연산자는 호스트 연산자로 분리해 컴파일 결과물 안에서 실행합니다. RBLN Compiler Python API의 RBLNCompiledModel.inspect()는 이를 host_op_reason에 표시합니다.
  • 프런트엔드가 변환할 수 없는 연산자가 있으면 NotImplementedError: The following operators are not implemented: [...]로 컴파일에 실패합니다. 기본 설정에서는 compile_error fallback이 전체 그래프를 CPU에서 실행합니다. 단계가 디바이스에 머무는지 확인할 때는 이 fallback을 비활성화해 실패가 오류로 드러나게 해야 합니다(fallback 제어 참고).

단계 본문은 컴파일된 함수 하나로 구성하고 eager 연산자는 그 밖의 준비와 마무리에만 사용합니다.

  1. 상수와 가중치는 프로그램 시작 시 RBLN으로 한 번만 옮깁니다. 단계 안에서 텐서를 만들지 않습니다.
  2. 입력 버퍼는 torch.empty로 한 번 만들고 매 단계 copy_로 채웁니다. 이 복사가 단계의 유일한 H2D입니다. torch.rbln.bind_device_memory()를 사용하면 복사 시 전송하고 사용하지 않으면 버퍼를 사용하는 첫 NPU 연산자에서 전송합니다. 어느 쪽이든 단계당 한 번입니다. pinned 호스트 텐서의 non_blocking=True 복사는 버퍼 최초 사용 전까지만 비동기이므로 반복 단계에서 전송과 연산이 겹칠 것으로 가정해서는 안 됩니다.
  3. 단계 본문은 torch.compile(backend="rbln", dynamic=False)로 컴파일한 함수 하나입니다. 패딩이나 버킷팅으로 입력 shape를 고정하고 dtype 하나(float16 또는 bfloat16)를 사용합니다. 마지막 차원은 64의 배수로 맞추고 오프셋 0인 연속 입력을 전달합니다.
  4. 단계 안에서 호스트로 읽지 않습니다. 분기는 디바이스 연산으로 표현하고 로깅과 검증은 끝에서 처리합니다.
  5. 결과는 끝에서 .cpu()로 한 번 가져옵니다. 출력이 여러 개면 그래프 안에서 하나의 텐서로 묶어 한 번 다운로드합니다.
  6. eager 실행이 필요하면 TORCH_RBLN_DEPLOY=ON을 설정하고 모든 연산자에서 3번 조건을 충족하며 스칼라 피연산자를 텐서 상수로 바꿉니다.
  7. 단계를 torch.rbln.explain()으로 감싸고 host->device push와 physical d2h가 각각 한 번인 [clean] 상태인지 확인합니다.

예제: 호스트를 거치는 단계와 디바이스에 머무는 단계

같은 연산을 두 방식으로 작성하고 explain() 보고서를 비교합니다. 첫 단계에는 앞 표의 패턴이 여러 개 포함되어 있습니다.

import torch

dev = "rbln:0"
weight = torch.randn(128, 128, dtype=torch.float16, device=dev)
scale = torch.full((128, 128), 0.5, dtype=torch.float16, device=dev)


def step_eager(x_host):
    x = x_host.to(dev)  # 매 단계 H2D가 발생합니다.
    h = torch.relu(x @ weight)
    if float(h.max()) > 1e4:  # 값 확인: D2H가 발생하며 완료를 기다립니다.
        h = h * 0.5
    counts = torch.ones(
        128, 128, dtype=torch.int32, device=dev
    )  # 매 단계 상수를 생성합니다.
    h = h * counts.to(torch.float16)  # 호스트를 거쳐 dtype을 변환합니다.
    return (h * scale)[:, :100]  # 마지막 차원이 64의 배수가 아닙니다.


def step_body(x):
    return torch.relu(x @ weight) * scale


step_graph = torch.compile(step_body, backend="rbln", dynamic=False)

x_host = torch.randn(128, 128, dtype=torch.float16)

with torch.inference_mode():
    # 워밍업: 두 경로를 한 번씩 실행해 컴파일 신호를 제외합니다.
    step_eager(x_host).cpu()
    step_graph(x_host.to(dev)).cpu()

    with torch.rbln.explain(with_stack=True) as bad:
        result = step_eager(x_host).cpu()

    x_dev = torch.empty(128, 128, dtype=torch.float16, device=dev)
    torch.rbln.bind_device_memory(x_dev)
    x_dev.copy_(x_host)
    step_graph(x_dev).cpu()  # 워밍업: 입력 버퍼를 그래프에 한 번 연결합니다.

    with torch.rbln.explain(with_stack=True) as good:
        x_dev.copy_(x_host)  # 단계 시작: H2D 한 번
        result = step_graph(x_dev).cpu()  # 단계 종료: D2H 한 번

print(bad.report())
print(good.report())

호스트를 거치는 단계의 보고서 일부입니다. 시간, 메모리 수치, 호스트 환경 행은 생략했습니다.

[overhead: 1 signal]  RBLN EXPLAIN   (region wall ... | device mem ... peak, reserved)

  host->device push (runtime): 3 pushes, ...

--------------------  -----  -----  -----------------------------------------------------------
Signal                Count  Bytes  Note
--------------------  -----  -----  -----------------------------------------------------------
host_bounce/d2d_copy      1    ...  real d2h DMA (costly) | try: make contiguous, or v2v engine
--------------------  -----  -----  -----------------------------------------------------------

  host_bounce/d2d_copy:
    at example.py:14(step_eager) <- ...

  >> physical d2h (real transfer): 3 copies, ... -- real device crossing

다음의 디바이스에 머무는 단계가 H2D와 D2H를 각각 한 번 수행하는 데 비해, 이 단계는 H2D 업로드 세 번과 실제 D2H 전송 세 번을 수행합니다. 입력 업로드, 단계 안에서 만드는 상수, dtype 변환, 값 검사, 결과 다운로드가 모두 영향을 줍니다. host_bounce/d2d_copy의 위치는 int32 상수를 float16으로 변환하는 행을 가리킵니다.

디바이스에 머무는 단계의 보고서입니다.

[clean]  RBLN EXPLAIN   (region wall ... | device mem ... peak, reserved)

  rebel runtime: ... in librbln
    v2h .../1  h2v .../1
  host->device push (runtime): 1 pushes, ...
  checked: host_bounce, v2v_slow, cpu_fallback, recompile -- none fired
  note: clean = no hidden host overhead, not "fast"

입력 업로드(h2v, 한 번)와 결과 다운로드(v2h)만 남았으며 추적하는 신호는 발생하지 않았습니다. 입력 버퍼를 그래프에 처음 연결하는 호출에서는 런타임이 그래프 입력 레이아웃에 맞춰 버퍼를 재배치하므로 전송이 한 번 더 발생합니다. 예제에서는 이 호출을 워밍업으로 제외했습니다. [clean]은 숨은 호스트 왕복 전송이 없다는 뜻입니다. 단계의 속도를 보장하는 것은 아니며 NPU 내부 시간은 RBLN Profiler로 분석합니다.

실행 경로 검증

CUDA에서는 영역이 느린 이유를 torch.profiler나 nsys로 확인합니다. RBLN에서 예상과 다른 성능의 원인은 대부분 숨은 호스트 작업입니다. 호스트를 거치는 복사, CPU로 fallback한 연산자, 웜 캐시 미스 등이 이에 해당합니다. torch.rbln.explain()은 영역 안의 eager 디스패치와 호스트를 거치는 복사를 관찰하고 각 이벤트의 원인을 표시합니다. 시간 프로파일러는 아니므로 [clean] 영역도 연산량 때문에 느릴 수 있습니다. 실제 경과 시간은 torch.profiler로 측정합니다. 트레이스에 RBLN 디바이스 행이 표시되는 조건은 API 및 지원 연산 › Explain과 프로파일링에서 설명합니다.

다음 예는 영역의 첫 실행과 반복 실행을 비교합니다. float16 덧셈은 서로 다른 텐서 두 개를 사용해 eager 웜 캐시를 재사용합니다. int32 곱셈은 dtype 검사를 통과하지 못해 CPU에서 실행됩니다. 두 경로에 서로 다른 연산자를 사용하므로 호출 위치를 구분할 수 있습니다.

import torch

x = torch.randn(128, 128, dtype=torch.float16, device="rbln:0")
y = torch.randn_like(x)
counts = torch.ones(128, 128, dtype=torch.int32, device="rbln:0")
steps = torch.full((128, 128), 2, dtype=torch.int32, device="rbln:0")

with torch.inference_mode():
    with torch.rbln.explain(with_stack=True) as first:
        fp_result = x + y
        int_result = counts * steps

    with torch.rbln.explain(with_stack=True) as repeated:
        fp_result = x + y
        int_result = counts * steps

print(first.report())
print(repeated.report())

덧셈의 웜 캐시 항목이 없는 프로세스에서 나온 보고서 일부입니다. 시간과 호출 위치는 ...으로 표시했습니다.

[overhead: 2 signals]  RBLN EXPLAIN   (region wall ... | device mem ... peak, reserved)

---------------------  -----  -----  -------------------------------------
Signal                 Count  Bytes  Note
---------------------  -----  -----  -------------------------------------
dispatch/cpu_fallback      1     --  try: graph mode, or a supported dtype
dispatch/recompile         1     --  try: stabilize shapes, or graph mode
---------------------  -----  -----  -------------------------------------

  dispatch/cpu_fallback:
    aten::mul.out  1  at ...
    why: dtype-not-fp16 1

  dispatch/recompile:
    aten::add.out  1  at ...
[overhead: 1 signal]  RBLN EXPLAIN   (region wall ... | device mem ... peak, reserved)

---------------------  -----  -----  -------------------------------------
Signal                 Count  Bytes  Note
---------------------  -----  -----  -------------------------------------
dispatch/cpu_fallback      1     --  try: graph mode, or a supported dtype
---------------------  -----  -----  -------------------------------------

  dispatch/cpu_fallback:
    aten::mul.out  1  at ...
    why: dtype-not-fp16 1

첫 보고서에는 덧셈의 dispatch/recompile과 곱셈의 CPU fallback이 표시됩니다. 반복 실행에서는 덧셈이 웜 캐시를 사용하므로 dispatch/recompile이 사라지고 fallback만 남습니다. 첫 보고서의 신호는 프로세스가 이전에 캐시한 내용에 따라 달라집니다.

보고서는 다음과 같이 해석합니다.

관찰 결과 확인할 사항
dispatch/cpu_fallback 연산자 이름, 입력 dtype, why: 이유를 확인합니다. 결과 텐서는 여전히 rbln 디바이스로 표시될 수 있습니다.
반복 eager 영역의 dispatch/recompile 웜 캐시를 사용하지 못한 호출입니다. 입력 프로파일과 앞의 캐시 조건을 확인합니다. 카운터는 eager 디스패치의 웜 캐시 미스 횟수입니다. 캐시 미스에서도 Python 컴파일 캐시를 재사용할 수 있으므로 컴파일러 호출 횟수와 다르며 torch.compile 그래프의 컴파일이나 재컴파일은 집계하지 않습니다.
호스트 스테이징과 실제 디바이스-호스트 전송량이 함께 표시됨 영역 안에서 데이터가 실제로 호스트로 이동했습니다.
호스트 스테이징만 있고 실제 디바이스-호스트 전송량은 없음 호스트의 데이터 복사본을 재사용한 경로입니다. DMA 비용은 실제 전송량으로 판단합니다.
[clean] 영역에서 추적 대상 오버헤드가 기록되지 않았습니다.

컴파일된 순전파만이 아니라 단계 전체를 감쌉니다. 컴파일된 그래프는 구조상 clean이며 숨은 작업은 주변 eager 코드에 있으므로 전처리와 후처리도 영역에 포함합니다. with_stack=True는 연산자 이름마다 처음 관찰한 호출 위치를 기록합니다. 여러 곳에서 호출한 연산자는 이후 신호에도 첫 위치가 표시되므로 호출 위치를 구분하려면 각각 별도 영역에서 관찰해야 합니다. 유효한 영역의 나머지 규칙은 API 및 지원 연산 › Explain과 프로파일링에서 확인할 수 있습니다.

CPU fallback 카운터는 계측된 eager 디스패치 경로만 집계합니다. 값이 0이어도 모든 연산자가 NPU에서 실행되었다는 뜻은 아닙니다. 확인할 수 있는 신호는 로드한 런타임에 따라 달라집니다. 런타임 신호가 제공되지 않으면 헤더에 표시되므로, 이 경우 runtime/v2v_slow 행이 없다는 이유만으로 해당 경로가 실행되지 않았다고 판단해서는 안 됩니다.

전송 카운터는 관찰한 경로만 집계합니다. 비동기 경로를 사용한 non_blocking=True 복사는 집계하지 않으며 동기 방식으로 전환된 복사는 host->device push에 집계합니다. 디바이스 메모리 최대 사용량은 캐시 버퍼를 포함한 런타임 예약 메모리입니다. explain_steady(fn)은 호출 가능한 객체를 두 영역에서 실행해 첫 호출의 컴파일과 반복 오버헤드를 구분합니다. 튜토리얼에서 두 경우를 살펴볼 수 있으며 오류 조사는 문제 해결에서 확인할 수 있습니다.