NDD Day 2 Lab — PyTorch Native & NKI 커널 최적화¶
실습 목표¶
| # | 목표 | 완료 기준 |
|---|---|---|
| 1 | PyTorch Native에서 Neuron 디바이스로 텐서 이동 및 연산 수행 | to('neuron') + 연산 결과 확인 |
| 2 | Eager 모드로 학습 루프 실행 (GPU 코드와 동일) | Loss 감소 확인 |
| 3 | torch.compile(backend="neuron")으로 NEFF 생성 및 성능 비교 |
Eager 대비 속도 향상 확인 |
| 4 | NKI Library 커널로 모델 핵심 연산 교체 후 성능 측정 | base → core 1.2x 성능 향상 |
| 5 | 프로파일링으로 CPU fallback 및 병목 구간 확인 | fallback ops 목록 + 프로파일 결과 |
총 소요: ~75분 (Lab 1: 35분 + Lab 2: 40분)
실습 환경¶
| 항목 | 값 |
|---|---|
| 인스턴스 | trn2.3xlarge (Trainium2 x 1 chip, 4 logical NeuronCores, HBM 96GB) |
| AMI | Deep Learning AMI Neuron (Ubuntu 24.04) — Neuron SDK 2.31 |
| PyTorch Native | neuronx-cc 2.26, nki 0.5.0, torch 2.11.0, torch-neuronx 2.11.3 |
| 가상환경 | ~/native_workshop_venv/ |
| Lab 1 코드 | ~/workspace/lab1/ |
| Lab 2 코드 | ~/workspace/lab2/ |
PyTorch Native란?
PyTorch Native는 Neuron의 차세대 PyTorch 통합 방식입니다. 기존 XLA 기반이 아닌, PyTorch의 PrivateUse1 디바이스 확장 메커니즘을 사용하여 to('neuron')으로 직접 디바이스에 접근합니다. GPU에서 to('cuda')를 쓰듯 동일한 패턴으로 동작합니다.
Lab 0: 환경 확인 (5분)¶
0-1. Trainium2 하드웨어 확인¶
예상 출력:
instance-type: trn2.3xlarge
+--------+--------+----------+--------+--------------+----------+------+
| NEURON | NEURON | NEURON | NEURON | PCI | CPU | NUMA |
| DEVICE | CORES | CORE IDS | MEMORY | BDF | AFFINITY | NODE |
+--------+--------+----------+--------+--------------+----------+------+
| 0 | 4 | 0-3 | 96 GB | 0000:33:00.0 | 0-11 | 0 |
+--------+--------+----------+--------+--------------+----------+------+
4 logical NeuronCores (LNC2 모드: 물리 8 → 논리 4)와 96 GB HBM을 확인합니다.
0-2. Python 가상환경 활성화¶
프롬프트가 (native_workshop_venv)로 바뀌면 성공입니다.
모든 터미널에서 이 venv를 활성화해야 합니다.
새 터미널을 열 때마다 반드시 실행하세요. Day 1에서 사용한 vLLM venv와는 완전히 별개 입니다. Lab 1과 Lab 2 모두 동일한 이 venv를 사용합니다.
0-3. Neuron SDK 설치 확인¶
예상 출력:
neuronx-cc 2.26.6360.0+6f180f47
nki 0.5.0+28631259367.ga768afa6
torch 2.11.0
torch-neuronx 2.11.3.0.1417+1431f083.dev
핵심 패키지:
torch— 표준 PyTorch (upstream)torch-neuronx— Neuron 디바이스 백엔드 플러그인 (PrivateUse1)neuronx-cc— Neuron 컴파일러 (FX Graph → NEFF)nki— Neuron Kernel Interface
0-4. PyTorch import 확인¶
OperatorEntry 경고는 정상입니다
처음 import torch를 실행하면 다음과 같은 경고가 출력됩니다:
[W] Warning only once for all operators, other operators may also be overridden. Overriding a previously registered kernel for the same operator and the same dispatch key operator: aten::gather.out(...) dispatch key: PrivateUse1
이는 Neuron 백엔드가 **~2000개 ATen op을 PrivateUse1 키로 등록**하는 정상 과정입니다. 기존 CPU 기본 커널을 Neuron 커널로 덮어씌우는 것이며, 이 경고가 뜨면 백엔드가 정상 로드된 것입니다. 한 번만 출력되고 이후에는 나타나지 않습니다.
예상 출력
$ python -c "import torch; print(torch.__version__); t = torch.randn(2,2).to('neuron'); print(t)"
[W818 08:33:06.095561353 OperatorEntry.cpp:208] Warning: Warning only once for all operators, other operators may also be overridden.
Overriding a previously registered kernel for the same operator and the same dispatch key
operator: aten::gather.out(Tensor self, int dim, Tensor index, *, bool sparse_grad=False, Tensor(a!) out) -> Tensor(a!)
registered at /pytorch/build/aten/src/ATen/RegisterSchema.cpp:6
dispatch key: PrivateUse1
previous kernel: registered at /pytorch/build/aten/src/ATen/RegisterCPU_3.cpp:7637
new kernel: registered at NeuronDispatcher.cpp:0 (function operator())
2.11.0+cu130
tensor([[-0.4285, 1.3709],
[ 0.1188, 0.0435]], device='neuron:0')
Lab 1: PyTorch Native 시작하기 (35분)¶
학습 목표
PyTorch Native에서 Neuron 디바이스를 사용하는 핵심 패턴을 직접 체험합니다: (1) 텐서 기본 연산, (2) Eager 학습, (3) torch.compile 최적화, (4) NKI 커널 통합.
Step 1: Device & Tensor Basics (~5분)¶
목표: PyTorch에서 Neuron 디바이스에 텐서를 올리고 연산하는 방법을 확인합니다.
Python 인터프리터를 열고 라인별로 따라해보세요:
>>> import torch
# CPU 텐서 생성
>>> cpu_tensor = torch.randn(3, 3)
>>> print(cpu_tensor.device)
cpu
# Neuron 디바이스로 이동
>>> neuron_tensor = cpu_tensor.to('neuron')
>>> print(neuron_tensor.device)
neuron:0
# Neuron에서 직접 연산 (CPU로 돌아갈 필요 없음)
>>> result = neuron_tensor * 2 + 1
>>> print(result)
tensor([[...]], device='neuron:0')
NKI_ENABLE_TRACE_CACHE 워닝이 보이나요?
UserWarning: NKI_ENABLE_TRACE_CACHE=1: the NKI kernel compile cache is persisted across processes...
이것은 정상 입니다. NKI 커널 컴파일 결과를 디스크에 캐시하여 두 번째 실행부터 빠르게 로드하겠다는 알림입니다. 실행에 영향 없으므로 무시하세요 .
# (계속)
# 디바이스 무관(Device-agnostic) 코드 — GPU/Neuron 양쪽에서 동작
>>> device = torch.accelerator.current_accelerator()
>>> print(device)
neuron:0
>>> portable_tensor = torch.randn(3, 3, device=device)
>>> print(portable_tensor.device)
neuron:0
>>> exit()
핵심 포인트:
| GPU | Neuron |
|---|---|
tensor.to('cuda') |
tensor.to('neuron') |
torch.device('cuda') |
torch.device('neuron') |
torch.cuda.synchronize() |
torch.neuron.synchronize() |
torch.accelerator.current_accelerator()를 사용하면 GPU/Neuron 이식 가능한 코드를 작성할 수 있습니다.
Step 2: Eager Mode Training (~10분)¶
목표 : 표준 PyTorch 학습 루프가 Neuron에서 그대로 동작하는 것을 확인합니다.
예상 출력:
체크포인트
Loss가 Epoch마다 꾸준히 감소하면 성공입니다. GPU 학습 코드와 100% 동일한 구조 임을 확인하세요 — 바꾼 것은 device = torch.device('neuron') 한 줄뿐입니다.
코드 핵심 (eager_mode.py):
device = torch.device('neuron')
model = nn.Sequential(nn.Linear(10, 2)).to(device)
X = torch.randn(100, 10).to(device)
y = torch.randint(0, 2, (100,), dtype=torch.int32).to(device)
for epoch in range(10):
output = model(X)
loss = criterion(output, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
int32 사용
Neuron은 int64를 지원하지 않습니다. Label tensor는 dtype=torch.int32로 생성하세요.
Step 3: torch.compile with Neuron Backend (~15분)¶
목표: torch.compile(backend="neuron")으로 모델을 컴파일하여 NEFF가 생성되고 정상 실행되는 것을 확인합니다. 첫 실행 시 컴파일 시간이 발생하고, 이후 캐시됩니다.
예상 출력:
torch.compile의 원리
torch.compile(backend="neuron")을 호출하면 내부적으로:
1. TorchDynamo — Python bytecode를 캡처해 FX Graph 추출
2. AOT Autograd — Forward/Backward 그래프 분리
3. Neuron Backend — FX → Neuron IR 변환
4. neuronx-cc — HW 최적화 → NEFF (실행 바이너리) 생성
전체 forward가 하나의 NEFF로 융합되어 op별 dispatch overhead가 제거됩니다.
Step 3 Additional: 비동기 실행과 synchronize() 이해¶
Neuron은 비동기 실행 모델입니다. CPU는 연산을 "발사"하고 즉시 다음 줄로 넘어갑니다. 정확한 벤치마크를 위해서는 반드시 torch.neuron.synchronize()로 완료를 대기해야 합니다.
torch.neuron.synchronize() # fence
start = time.perf_counter()
for _ in range(100):
output = model(x)
torch.neuron.synchronize() # 완료 대기
elapsed = time.perf_counter() - start
synchronize() 없이 측정하면?
CPU dispatch 시간만 측정됩니다 — NeuronCore의 실제 연산 시간이 아닌, 명령을 보내는 시간만 재는 것이므로 무의미합니다.
Step 4: NKI 커널 통합 (~5분)¶
목표 : @nki_op 패턴으로 NKI 커널을 PyTorch에 통합하는 기초를 확인합니다.
예상 출력:
Eager mode - NKI add result shape: torch.Size([128]), device: neuron:0
Compiled mode - NKI add result shape: torch.Size([128]), device: neuron:0
체크포인트
Eager 모드와 Compiled 모드 모두에서 NKI 커널이 정상 호출되면 성공입니다. 이것이 Lab 2에서 본격적으로 사용할 @nki_op 패턴의 기초입니다.
Step5: 분산 학습 (FSDP)¶
목표: torchrun으로 4 NeuronCore를 활용한 분산 학습이 동작함을 확인합니다.
NEURON_RT_VIRTUAL_CORE_SIZE=2 \
NEURON_RT_NUM_CORES=4 \
torchrun \
--nproc_per_node 4 \
--rdzv_backend c10d \
--rdzv_endpoint localhost:29500 \
distributed_fsdp.py
예상 출력:
[Rank 0/4] Initialized on neuron:0
[Rank 1/4] Initialized on neuron:1
[Rank 2/4] Initialized on neuron:2
[Rank 3/4] Initialized on neuron:3
Iteration 1/5, Loss: 1.0071
...
Iteration 5/5, Loss: 0.9876
Training complete!
EFA 경고는 무시하세요
NET/OFI Failed to initialize rdma protocol — trn2.3xlarge에는 EFA가 없어서 나오는 경고입니다. 노드 내 통신은 NeuronLink로 처리되므로 정상 동작에 영향 없습니다.
Additional: SFT Fine-tuning¶
목표: 실제 LLM (Qwen3-1.7B)을 PyTorch Native에서 fine-tuning할 수 있음을 확인합니다.
# LoRA Fine-tuning (빠름, 메모리 효율적)
./run_lora_finetune_hf.sh
# Full Fine-tuning (전체 파라미터 업데이트)
./run_full_finetune_hf.sh
예상 출력
-
컴파일 시간 경과 후(약 5분~10분) Loss 가 감소하는 것을 확인하세요
-
LoRA Fine-tuning
WARNING:torch_neuronx.python_ops.dtype_autocast:Neuron backend does not support int64. Automatically casting to int32. Consider using int32 directly for better performance until native int64 support is added. WARNING:torch_neuronx.python_ops.dtype_autocast:Neuron backend does not support int64. Automatically casting to int32. Consider using int32 directly for better performance until native int64 support is added. step=10/92 loss=17.0549 tok/s=6446 ms/step=5083.7 step=20/92 loss=12.6415 tok/s=6519 ms/step=5026.2 step=30/92 loss=7.7104 tok/s=6549 ms/step=5003.2 step=40/92 loss=5.3484 tok/s=6447 ms/step=5082.5 step=50/92 loss=4.4552 tok/s=6630 ms/step=4942.2 step=60/92 loss=3.6861 tok/s=6659 ms/step=4920.9 step=70/92 loss=2.9134 tok/s=6652 ms/step=4926.1 step=80/92 loss=2.5239 tok/s=6672 ms/step=4911.5 step=90/92 loss=2.2648 tok/s=6634 ms/step=4939.4 Saving final model shards to Qwen3-1.7B-LoRA-Python-Coder/shard-{0..3} Training complete ========================================== Training completed. LoRA adapter saved to: Qwen3-1.7B-LoRA-Python-Coder ========================================== -
Full Fine-tuning
directly for better performance until native int64 support is added. WARNING:torch_neuronx.python_ops.dtype_autocast:Neuron backend does not support int64. Automatically casting to int32. Consider using int32 directly for better performance until native int64 support is added. WARNING:torch_neuronx.python_ops.dtype_autocast:Neuron backend does not support int64. Automatically casting to int32. Consider using int32 directly for better performance until native int64 support is added. step=10/92 loss=19.9439 tok/s=743.4 ms/step=44077.9 step=20/92 loss=19.6373 tok/s=6852.1 ms/step=4782.2 step=30/92 loss=19.4491 tok/s=6821.1 ms/step=4803.9 step=40/92 loss=19.0041 tok/s=6884.8 ms/step=4759.5 step=50/92 loss=17.8819 tok/s=6894.3 ms/step=4752.9 step=60/92 loss=16.6132 tok/s=6878.2 ms/step=4764.0 step=70/92 loss=15.4918 tok/s=6888.2 ms/step=4757.1 step=80/92 loss=14.4292 tok/s=6844.9 ms/step=4787.2 step=90/92 loss=12.9341 tok/s=6881.7 ms/step=4761.6 Saving final model to Qwen3-1.7B-Full-Finetuned-HF Writing model shards: 100%|██████████████████████████████████████████████████████████████████████████████████| 1/1 [00:06<00:00, 6.15s/it] Training completed ========================================== Training completed. Model saved to: Qwen3-1.7B-Full-Finetuned-HF ==========================================
PyTorch Native의 핵심 가치: GPU에서 쓰던 HuggingFace + PEFT 학습 코드가 그대로 동작합니다.
Lab 2: NKI로 모델 최적화 하기 (40분)¶
학습 목표
NKI Library의 최적화된 커널로 모델의 핵심 연산을 교체하고, 성능 향상을 직접 측정합니다. 코드 10줄 변경으로 1.5x 성능 향상을 달성합니다.
Lab 2 코드 구조¶
~/workspace/lab2/
├── run.py ← 실행 진입점 (모델 선택, 컴파일, 벤치마크)
├── nki_ops.py ← @nki_op 등록 코드 (wrap_nki + nki_op 패턴)
├── profile_utils.py ← 프로파일링 유틸리티
├── llama_base/ ← 순수 PyTorch Llama (기준선)
│ ├── llama.py ← 모델 전체 (forward loop)
│ ├── self_attention.py ← Attention 구현
│ └── transformer_block.py ← 레이어 블록 (Attn + MLP)
├── llama_nki_core/ ← nkilib core 커널로 MLP+RoPE 교체
│ ├── llama_nki.py
│ ├── self_attention.py
│ └── transformer_block.py
└── llama_nki_experimental/ ← nkilib mega-kernel (Attention 포함 전체)
├── llama_nki.py
├── self_attention.py
└── transformer_block.py
핵심 구조:
폴더 | 역할 | 바뀌는 것 |
|---|---|---|
| 순수 PyTorch — | 없음 (기준선) |
| base와 동일하되, MLP/RoPE를 |
|
| Attention까지 통째로 mega-kernel로 교체 |
|
| nkilib import → wrap_nki → @nki_op 등록 | S2에서 배운 패턴 그대로 |
run.py가 하는 일:
-
--model base/core/experimental인자로 모델 폴더 선택 -
--compile있으면torch.compile(backend="neuron")적용 -
--greedy있으면 greedy decoding (token generation) -
Warmup 3회 → Timing 50 tokens → throughput 출력
3개 폴더의 관계:
llama_base/ → 모든 op이 PyTorch 기본 (HBM 왕복 5회)
llama_nki_core/ → MLP, RoPE를 nkilib 커널로 교체 (HBM 왕복 3회)
llama_nki_experimental/ → 레이어 전체를 1개 mega-kernel (HBM 왕복 1회)
즉, 여러분이 바꾸는 건 --model 인자뿐이고, 코드 내부에서는 동일한 weight에 같은 입력을 넣되 forward() 경로만 달라집니다. nki_ops.py를 열어보면 S2에서 배운 @nki_op + wrap_nki 패턴이 그대로 사용됩니다.
LLM 레이어 구성 요소 — 왜 이걸 교체하는가?
Transformer의 레이어 1개는 이렇게 생겼습니다:
입력 → [RMSNorm] → [QKV Linear] → [RoPE] → [Attention] → [Output Linear] → [RMSNorm] → [MLP(SwiGLU)] → 출력
| 구성 요소 | 하는 일 (한 줄) |
|---|---|
| RMSNorm | 텐서 크기를 정규화 (학습 안정화) |
| QKV Linear | 입력을 Query, Key, Value 3개로 변환 |
| RoPE | 토큰 위치 정보를 회전(rotation)으로 인코딩 |
| Attention | "어디를 볼까" — Q×K로 유사도 계산 후 V에서 정보 추출 |
| MLP (SwiGLU) | "정보를 변환" — gate로 걸러서 비선형 변환 (Linear 3개 + SiLU) |
문제: PyTorch 기본 코드에서는 각 구성 요소 사이마다 결과를 HBM에 저장하고 다음이 다시 읽음.
base: [RMSNorm]↕HBM → [QKV]↕HBM → [RoPE]↕HBM → [Attn]↕HBM → [MLP] = 5회 왕복
core: [RMSNorm+QKV]↕HBM → [RoPE+Attn]↕HBM → [MLP(nki)] = 3회 왕복
experimental: [전부 SBUF 안에서 처리] = 1회 왕복
NKI 커널이 하는 일 = 이 HBM 왕복을 줄이는 것. GPU에서도 동일한 문제가 있어서 Flash Attention, CUDA Fused Kernel이 존재합니다. Neuron에서는 SBUF(28MB)가 GPU Shared Memory(수백KB)보다 훨씬 크므로, 더 넓은 범위의 Fusion이 가능합니다.
Step 1: Baseline 실행 (~5분)¶
목표: 순수 PyTorch로 작성된 Llama 모델의 기준 성능을 확인합니다.
예상 출력:
Model : base — Baseline (pure PyTorch ops, RoPE, static KV cache)
Size : small
Compile : True
Tokens : 50
Params : 41,556,480 (79.3 MB bfloat16)
Warmup...
Timing...
Total time : 0.139s
Tokens generated: 50
Throughput : 359.0 tok/s
체크포인트
약 359 tok/s 전후의 throughput을 확인하세요. 이것이 NKI 없는 기준선입니다.
Step 2: 코드 분석 — "뭘 바꿨길래?" (~10분)¶
목표: base와 core의 코드 차이를 분석하고, NKI 커널 교체가 무엇인지 이해합니다.
핵심 Diff — MLP (FeedForward):
def forward(self, x):
gate = F.silu(self.gate_proj(x))
up = self.up_proj(x)
down = self.down_proj(gate * up)
return down
from nki_ops import nki_mlp
def forward(self, x):
return nki_mlp(
x,
self.gate_proj.weight,
self.up_proj.weight,
self.down_proj.weight
)
왜 빨라지는가?
순수 PyTorch에서는 gate, up, down 각각이 별도 연산으로 실행됩니다. 매번 HBM 왕복이 발생합니다.
nki_ops.py 확인 (NKI 등록 패턴):
핵심 패턴: wrap_nki() + @nki_op → PyTorch custom op으로 등록
Step 3: Core 실행 (~5분)¶
예상 출력:
체크포인트
약 435 tok/s — base 대비 1.2x 향상. 코드 ~10줄 변경으로 달성한 결과입니다.
Step 4: Experimental — Mega-kernel (~5분)¶
예상 출력:
Model : experimental — NKI experimental (CTE prefill + megakernel decode, static KV cache)
Throughput : 537.4 tok/s
체크포인트
약 537 tok/s — base 대비 1.5x 향상. Experimental은 Transformer block 전체를 하나의 mega-kernel로 융합합니다.
Step 5: 성능 비교 정리¶
| 변종 | Throughput | base 대비 | 변경 내용 |
|---|---|---|---|
| base | 359 tok/s | 1.0x | 순수 PyTorch (Graph Compiler만) |
| core | 435 tok/s | 1.2x | MLP + Attention NKI 커널 교체 |
| experimental | 537 tok/s | 1.5x | Mega-kernel (전체 block 융합) |
Step 6: 프로파일링 — CPU fallback 확인¶
목표: 모델에서 어떤 op이 NeuronCore에서 실행되고, 어떤 op이 CPU로 빠지는지 확인합니다.
run.py에 fallback 체크 코드 추가¶
run.py의 Generated IDs 출력 바로 다음 줄에 **한 줄**만 추가합니다:
torch_neuronx는 이미 import 되어있으므로 추가 import 불필요. 인덴트는 위 print문과 동일 (스페이스 4칸).
Greedy 모드 실행¶
기대 출력 (마지막 줄):
→ argmax도 CPU fallback이지만, 1회 호출로 throughput 영향은 미미합니다 (426 tok/s).
Sampling 모드 실행 (성능 영향 큰 fallback)¶
기대 출력 (마지막 줄):
→ top-p 필터링에 필요한 sort와 multinomial이 CPU로 빠지면서 매 토큰마다 HBM↔CPU 왕복 발생 → 52 tok/s (8배 느림!).
왜 이런 차이가 나는가?
| 모드 | Fallback ops | Throughput | 영향 |
|---|---|---|---|
--greedy |
argmax.out (1개) |
426 tok/s | 미미 (토큰당 1회) |
| sampling | sort + multinomial (2개) |
52 tok/s | 심각 (매 토큰 반복) |
같은 모델, 같은 compile인데 **CPU fallback ops 차이로 8배 성능 차이**가 발생합니다.
실무에서는 vLLM이 sampling을 자체 처리하므로 이 fallback은 문제되지 않습니다. 여기서는 **fallback을 감지하는 방법**을 익히는 것이 목적입니다.
핵심 API
현재 프로세스에서 CPU로 빠진 op의 누적 목록을 리턴합니다. 모델 코드 수정 없이, 실행 후 호출하면 됩니다.Step 7: Code Diff 패턴 정리¶
모든 NKI 커널 교체는 동일한 패턴을 따릅니다:
| 단계 | 파일 | 작업 |
|---|---|---|
| 1 | nki_ops.py |
wrap_nki() + @nki_op으로 커널 등록 |
| 2 | transformer_block.py |
MLP forward() 교체 |
| 3 | self_attention.py |
Attention forward() 교체 |
| 4 | 실행 | python run.py --model core --compile --greedy |
| 5 | 검증 | throughput 비교 |
실습 체크리스트:
nki_ops.py에서 사용할 커널 확인 (nki_mlp, nki_rmsnorm, nki_attn 등)- weight shape 확인 —
.weight직접 전달 (nkilib이 내부에서 transpose 처리) - 기존 forward() 호출을 nki 함수 호출로 교체
- 실행 → 정확도 검증 → 벤치마크 비교
마무리¶
오늘 배운 것
- Lab 1 : PyTorch Native에서
.to('neuron')한 줄로 GPU 코드가 Neuron에서 동작 - Lab 2 : nkilib 커널 교체 10줄로 1.5x 성능 향상 — 커널을 직접 작성하지 않아도 됨
- 핵심 메시지 : NKI는 "처음부터 작성"이 아닌 "검증된 커널을 갖다 쓰는 것"부터 시작