콘텐츠로 이동

RBLN PyTorch 실행 및 디버깅

이 튜토리얼에서는 RBLN NPU에서 첫 연산과 첫 모듈을 실행하고, 실행 과정을 확인하는 두 도구인 torch.rbln.explain()과 CPU fallback 로그를 살펴봅니다. 연산이 디바이스에서 실행되었는지 구분하고, 그렇지 않을 때 어디를 확인해야 하는지 배웁니다.

사전 준비

  • RBLN Driver가 설치된 RBLN NPU 호스트
  • Python: 3.10–3.14
  • 설치 문서에 따라 설치한 torch, torch-rbln, rebel-compiler

아직 패키지를 설치하지 않았다면 다음 명령을 실행합니다.

1
2
3
4
5
$ pip install \
  --index-url https://download.pytorch.org/whl/cpu \
  torch==2.13.0+cpu
$ pip install \
  torch-rbln==0.12.0
1
2
3
$ pip install \
  --extra-index-url https://pypi.rbln.ai/simple \
  rebel-compiler==0.12.0

Note

rebel-compiler를 사용하려면 RBLN Portal 계정이 필요합니다.

시작하기 전에 환경을 확인합니다.

python -m torch_rbln.diagnose

보고서의 rebel ABI handshake: 블록은 verdict: OK로 끝나야 합니다. 그렇지 않으면 먼저 설치 › 설치 확인 절차를 따릅니다.

1단계: 첫 연산 실행

다음 add.py는 원소가 64개인 두 fp16 텐서를 NPU에서 더합니다. GPU나 CPU에서 사용하는 코드와 같으며, cuda나 cpu 대신 rbln을 지정합니다.

add.py
1
2
3
4
5
6
7
8
9
import torch

device = "rbln"

a = torch.arange(64, dtype=torch.float16, device=device)
b = torch.ones(64, dtype=torch.float16, device=device)

c = a + b
print(c.device, c.dtype, c[:4].cpu())  # expect: rbln:0, fp16, [1., 2., 3., 4.]
$ python3 add.py
rbln:0 torch.float16 tensor([1., 2., 3., 4.], dtype=torch.float16)

출력에는 나타나지 않지만 다음 두 작업이 수행되었습니다.

  • a와 b는 지연 생성되었습니다. rbln에 텐서를 생성하면 가상 주소만 예약합니다. a + b가 처음으로 디바이스에서 두 텐서를 필요로 할 때 실제 디바이스 메모리를 할당하고 데이터를 업로드했습니다.
  • a + b는 첫 호출에서 해당 shape과 dtype에 맞게 aten::add를 컴파일했습니다. 같은 프로세스에서 같은 shape과 dtype으로 다시 호출하면 컴파일된 프로그램을 재사용합니다. 캐시는 프로세스 내에 유지되므로 새 프로세스에서는 다시 컴파일합니다.

이 스크립트에는 두 가지 주목할 만한 디테일이 있습니다. eager 덧셈을 CPU가 아닌 NPU에서 실행하려면 입력 텐서의 마지막 차원 크기가 64의 배수여야 합니다. 이 예제는 1차원 텐서를 사용하므로 원소 수를 64로 설정했습니다(실행 방식 › 디스패치 전 검사 참고). 또 c 자체 대신 c[:4].cpu()를 출력합니다. rbln 텐서를 출력용으로 포맷하면 해당 텐서에 호스트 측 연산을 실행하므로, 먼저 .cpu()로 값을 가져와야 3단계의 fallback 로그에 불필요한 항목이 생기지 않습니다.

dtype도 중요합니다. 디바이스 커널은 float16과 bfloat16을 처리하며, 다른 dtype은 호스트에서 실행됩니다. torch.float16을 torch.float32로 바꿔도 결과는 올바르지만 덧셈은 CPU에서 실행됩니다. 3단계에서 이를 확인하는 방법을 살펴봅니다.

2단계: 디바이스에서 모듈 실행

모듈도 단일 연산과 같은 방식으로 실행합니다. 디바이스에서 지원하는 dtype으로 변환하고, rbln으로 옮긴 다음 호출합니다. module.py를 생성합니다.

import torch

net = (
    torch.nn.Sequential(
        torch.nn.Linear(64, 128),
        torch.nn.SiLU(),
        torch.nn.Linear(128, 64),
    )
    .to(torch.float16)
    .to("rbln")
    .eval()
)

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

with torch.inference_mode():
    y = net(x)

print(y.shape, y.dtype, y.device)
$ python3 module.py
torch.Size([4, 64]) torch.float16 rbln:0

어떤 모델을 포팅하든 다음 두 규칙을 따릅니다.

  • 텐서 이동 전에 dtype을 변환합니다. 먼저 .to(torch.float16)을 호출한 다음 .to("rbln")을 호출합니다. float32 모듈을 rbln에 배치해도 실행되지만, 모든 연산이 CPU로 fallback됩니다.
  • model.eval()로 모델을 추론 모드로 설정하고, torch.inference_mode()를 함께 사용해 불필요한 메모리 사용을 줄일 수 있습니다.

첫 호출에서 각 Linear와 SiLU를 컴파일했습니다. 같은 프로세스에서 순전파를 다시 실행하면 컴파일된 프로그램을 재실행합니다.

3단계: CPU에서 실행된 연산 확인

디바이스에서 지원하지 않는 연산이나 dtype을 만나면 RBLN PyTorch는 CPU에서 실행해 처리를 이어갑니다. 결과는 여전히 rbln 텐서이므로 tensor.device는 텐서가 배치된 위치를 나타내며, 연산이 실행된 위치를 나타내지는 않습니다. 실제 실행 과정을 확인하는 도구가 torch.rbln.explain()입니다. 직접 작성하지 않은 코드라 내부를 알기 어려울 때도 이 컨텍스트 관리자로 실행 구간을 확인할 수 있습니다. 백엔드의 CPU fallback, 재컴파일, 호스트 왕복 횟수와 각각의 원인을 알려줍니다.

add_fp32.py를 생성합니다. 1단계의 텐서를 float32로 바꾸고 explain() 구간으로 감싼 코드입니다.

import torch

device = "rbln"

a = torch.arange(64, dtype=torch.float32, device=device)
b = torch.ones(64, dtype=torch.float32, device=device)

with torch.rbln.explain(with_stack=True) as p:
    c = a + b

print(c.device, c.dtype, c[:4].cpu())
print(p.report())
$ python3 add_fp32.py
rbln:0 torch.float32 tensor([1., 2., 3., 4.])
[overhead: 1 signal]  RBLN EXPLAIN   (region wall 989.001us | device mem 256.00 MB peak, reserved)
  ...
---------------------  -----  -----  -------------------------------------
Signal                 Count  Bytes  Note
---------------------  -----  -----  -------------------------------------
dispatch/cpu_fallback      1     --  try: graph mode, or a supported dtype
---------------------  -----  -----  -------------------------------------
  dispatch/cpu_fallback:  (sum 850.661us wall)
    aten::add.out  1  at add_fp32.py:9(<module>)
    why: dtype-not-fp16 1
    candidates (no fast-path handler): add.out
  (detail: p.help(signal) | raw: p.dump())

결과는 올바르고 디바이스는 rbln:0으로 표시되지만, 덧셈은 CPU에서 실행되었습니다. 보고서는 다음과 같이 해석할 수 있습니다.

  • [overhead: N signals] 표시는 심각도 등급이 아니라 발생 사실을 나타냅니다. N종류의 숨은 이벤트가 발생했다는 뜻이며, 비용은 Count, Bytes, Note 열을 보고 판단합니다.
  • dispatch/cpu_fallback은 fallback된 연산과 횟수를 묶어 보여주며, why:는 이유를 표시합니다. 이 예제의 이유는 dtype-not-fp16입니다. 입력이 디바이스 커널을 지원하지 않는 float32이기 때문입니다. candidates 줄은 호스트 fast path가 없는 fallback 연산을 나열합니다.
  • with_stack=True는 연산별로 at file:line(function) 형태의 호출 위치를 추가합니다. 기본값은 비활성화 상태이므로 기본 구간에는 이 비용이 들지 않습니다.
  • Note는 먼저 시도할 조치를 안내합니다. 해결 방법이 없는 행은 런타임 내부 알림입니다.

Fallback은 두 경로에서 발생하며, 이 중 하나만 오류로 바꿀 수 있습니다.

  • RBLN에 전혀 등록되지 않은 연산은 일반 fallback 경로를 따릅니다. TORCH_RBLN_DISABLE_FALLBACK=unsupported_op를 설정하면 미등록 연산을 CPU에서 실행하는 대신 오류를 발생시킵니다. 오류 메시지에는 연산 이름이 포함됩니다. 이 환경 변수는 확장 로드 시 한 번만 읽으므로, 프로세스를 시작하기 전에 설정합니다.
  • 등록된 연산이 디스패치 전 검사를 통과하지 못하면 이 환경 변수와 관계없이 fallback됩니다. 여기서 aten::add는 입력이 float32라서 검사에 실패합니다. why: 줄에서 이를 확인하고 dtype을 변경하면 해결됩니다.

fallback 범주는 환경 변수 › 실행과 fallback에 정리되어 있습니다.

explain()은 한 구간을 대상으로 합니다. 전체 실행을 확인할 수 있도록 RBLN PyTorch는 INFO 수준으로 fallback 로그도 남깁니다. 기본 로그 수준인 WARNING에서는 이 로그가 표시되지 않습니다. 로그는 CPU에서 실행된 모든 연산의 이름을 기록합니다. 앞의 aten::add처럼 dtype 검사 등 디스패치 전 검사로 CPU에 전달된 연산도 포함합니다. 호출 위치를 추적할 수 있으면 소스 코드의 줄도 표시하므로, 모델에서 fallback을 일으킨 계층을 찾을 수 있습니다. 다만 이유는 알려주지 않습니다. explain()은 디스패치 전 검사에 따른 이유를 알려주지만, 디바이스 커널이 없거나 CPU에 고정된 연산은 집계하지 않으므로 이런 연산은 로그로만 확인할 수 있습니다. 다음 명령으로 로그를 활성화합니다.

export TORCH_RBLN_LOG_LEVEL=INFO

예를 들어 torch.sin에는 디바이스 커널이 없으므로 rbln 텐서에 torch.sin(x)를 실행하면 다음 로그가 남습니다.

[2026-09-14 19:49:00.145][I] `aten::sin` op ran on CPU instead of RBLN
[2026-09-14 19:49:00.145][W] TRACE
sin_demo.py:5: UserWarning: TRACE (Triggered internally at c10/rbln/RBLNLogging.cpp:266.)
  y = torch.sin(x)

Warning

pdb나 coverage 같은 Python tracer가 활성화되어 있으면 RBLN PyTorch는 tracer 아래에서 컴파일하는 대신 모든 연산을 CPU에서 실행합니다. 디버거에서 확인하는 값은 올바르지만, 이때 관찰한 디바이스 배치와 실행 시간은 일반 실행을 반영하지 않습니다. Fallback 로그와 explain() 보고서는 디버거 밖에서 실행해 확인해야 합니다.

확인을 마치면 기본 로그 수준으로 복원합니다.

export TORCH_RBLN_LOG_LEVEL=WARNING

4단계: 숨은 오버헤드 분석

3단계에서는 explain()으로 CPU fallback을 확인했습니다. 같은 보고서로 재컴파일과 호스트를 거치는 복사도 확인하고, 첫 호출에서 한 번만 드는 비용과 호출마다 반복되는 비용을 구분할 수 있습니다. 이 도구는 실행 시간을 측정하는 profiler가 아닙니다. 구간이 [clean]이어도 디바이스 연산 시간이 길면 느릴 수 있습니다.

2단계의 모듈을 explain()으로 감쌉니다. 첫 순전파에서는 각 연산을 컴파일하고 explain()이 이를 dispatch/recompile 신호로 보고합니다. 정상 상태의 동작을 확인하려면 구간을 열기 전에 모듈을 한 번 실행합니다.

import torch

net = (
    torch.nn.Sequential(
        torch.nn.Linear(64, 128),
        torch.nn.SiLU(),
        torch.nn.Linear(128, 64),
    )
    .to(torch.float16)
    .to("rbln")
    .eval()
)

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

with torch.inference_mode():
    net(x)  # 워밍업: 세 연산을 컴파일합니다

with torch.inference_mode(), torch.rbln.explain() as p:
    y = net(x)

print(p.report())

숨은 오버헤드가 없는 구간은 [clean] 표시와 함께 검사한 항목을 나열합니다. 비어 있는 보고서는 문제가 없다는 확인 결과입니다. 다음은 축약한 출력이며, 헤더의 실행 시간과 런타임 메모리 상주량은 머신마다 다릅니다.

[clean]  RBLN EXPLAIN   (region wall 801.076us | device mem 264.33 MB peak, reserved)
  ...
  checked: host_bounce, v2v_slow, cpu_fallback, recompile -- none fired
  note: clean = no hidden host overhead, not "fast"

이번에는 의도적으로 fallback을 일으킵니다. fp32 피연산자는 디바이스 dtype 정책에 맞지 않습니다. 이 피연산자를 만드는 dtype 변환은 호스트를 거치는 복사입니다.

1
2
3
4
with torch.rbln.explain(with_stack=True) as p:
    w = x.float() * 2.0

print(p.report())
[overhead: 2 signals]  RBLN EXPLAIN   (region wall 230.989us | device mem 264.33 MB peak, reserved)
  ...
---------------------  -----  -----  -------------------------------------------------------
Signal                 Count  Bytes  Note
---------------------  -----  -----  -------------------------------------------------------
host_bounce/d2d_copy       1  512 B  no DMA (low cost) | try: make contiguous, or v2v engine
dispatch/cpu_fallback      1     --  try: graph mode, or a supported dtype
---------------------  -----  -----  -------------------------------------------------------
  host_bounce/d2d_copy:
    at explain_demo.py:19(<module>)
  dispatch/cpu_fallback:  (sum 67.500us wall)
    aten::mul.out  1  at explain_demo.py:19(<module>)
    why: dtype-not-fp16 1
  >> physical d2h (real transfer): 0 -- everything above served on host, no crossing
  (detail: p.help(signal) | raw: p.dump())

이 도구를 통해 Python 코드 한 줄에서 두 개의 신호가 발생한 것을 확인할 수 있습니다. x.float()는 호스트를 왕복하는 디바이스 dtype 변환입니다(host_bounce/d2d_copy, 512 B). 변환된 float32 텐서의 * 2.0 연산은 CPU에서 실행되었습니다(dispatch/cpu_fallback). why:는 3단계와 같은 이유인 dtype-not-fp16을 표시합니다. 복사의 경우 >> physical d2h 줄에서 데이터가 실제로 디바이스 경계를 넘었는지 확인합니다. 여기서는 x가 여전히 호스트에 상주하고 있어 데이터 이동이 없었고, Note 열도 이 왕복의 비용을 낮게 평가합니다.

일반적인 작업 흐름에는 다음 두 호출도 사용합니다.

p.verdict()  # {'clean': False, 'reasons': ['hidden host bounce (torch-side): 1x, 512 B', 'cpu_fallback: 1x (ran on CPU)'], ...}  -> CI 검증 조건으로 사용합니다
p.dump()  # 모든 카운터를 원시 숫자로 반환합니다 -> 코드에서 검증합니다

한 번만 드는 비용인 첫 호출의 컴파일을 반복되는 오버헤드와 구분하려면 explain_steady()를 사용합니다. callable을 감싸는 두 구간을 만듭니다. 하나는 이 도구가 수행하는 첫 호출을 감싸고, 다른 하나는 warmup회 더 호출한 뒤의 호출을 감쌉니다.

# 새로운 계층 크기: 컴파일 캐시는 연산과 shape을 키로 사용합니다.
# 같은 종류의 모듈을 실행했어도 이 프로그램들은 아직 캐시에 없습니다.
net_wide = (
    torch.nn.Sequential(
        torch.nn.Linear(64, 256),
        torch.nn.SiLU(),
        torch.nn.Linear(256, 64),
    )
    .to(torch.float16)
    .to("rbln")
    .eval()
)

with torch.inference_mode():
    cold, warm = torch.rbln.explain_steady(
        lambda: net_wide(x), warmup=2, return_cold=True
    )

print(cold.verdict()["reasons"])
print(warm.verdict()["clean"])
['recompile/cold-compile: 3x']
True

첫 호출에서 세 연산을 컴파일했고, 이후 호출에서는 이를 재실행해 clean 상태가 되었습니다. 2단계와 계층 크기가 같은 두 번째 모듈이라면 컴파일이 전혀 발생하지 않았을 것입니다. 캐시의 키가 모듈 객체가 아니라 연산과 입력 shape이기 때문입니다. 두 구간 사이에서 사라진 신호는 한 번만 발생한 작업이고, 계속 남는 신호는 개선해야 할 반복 오버헤드입니다. cold.diff(warm)은 이 비교를 신호별 표로 보여줍니다(explain_steady(..., as_diff=True)는 같은 객체를 바로 반환합니다).

print(cold.diff(warm).report())
RBLN EXPLAIN DIFF   (A -> B; you placed both points)
  explain does not know your run structure; this compares ONLY your two regions.
--------------------  -  -  ---------  -------  -----------
Signal                A  B    A bytes  B bytes
--------------------  -  -  ---------  -------  -----------
host_bounce           0  0         --       --
runtime/v2v_slow      0  0    4.00 KB  4.00 KB
runtime/physical_d2h  2  2    4.00 KB  4.00 KB  >> persists
runtime/h2d_push      7  0  499.25 KB       --  gone in B
cpu_fallback          0  0         --       --
recompile             3  0         --       --  gone in B
--------------------  -  -  ---------  -------  -----------

이 도우미는 callable을 이전에 실행하지 않았으며 호출마다 같은 작업을 수행한다고 가정합니다. 매개변수는 API에서 확인할 수 있습니다.

다음 단계

  • 실행 방식 › 그래프 실행과 입력 shape - torch.compile(backend="rbln")로 해당 단계를 하나의 그래프로 컴파일해 디바이스에서 실행합니다.
  • 튜토리얼: Llama3.2-1B - 실제 모델에 2단계의 두 규칙과 3·4단계의 explain() 작업 흐름을 적용합니다. 생성 과정에서 무엇을 컴파일하는지 관찰한 다음 CPU 기준 결과와 비교해 검증합니다.
  • 실행 방식 - fp32가 fallback되는 이유, 지연 할당이 메모리 통계에 미치는 영향, torch.compile의 역할을 설명합니다.
  • API 및 지원 연산 - 앞에서 사용한 모든 torch.rbln 호출과 eager 연산 지원 표를 제공합니다.
  • 환경 변수 - 로깅, fallback 제어, 디바이스 매핑을 설명합니다.
  • RBLN vLLM - RBLN NPU에서 LLM을 서빙합니다. vLLM-native 경로는 torch-rbln 디바이스 텐서로 실행됩니다.