> EulerNPU > 튜토리얼 > 10. nanoGPT FFN-NPU — 실리콘 검증 (L2 하이브리드)

10. nanoGPT FFN-NPU — 실리콘 검증 (L2 하이브리드)

08 / 09 가 L3 단일-IP 패턴이었다면, 이 페이지는 그 패턴을 의도적으로 떠난 단계입니다. 진짜 LLM 을 작은 칩에 올리기 위한 L2 hybrid offload.

같은 보드 (QMTECH XC7Z020), 같은 툴체인, 다른 분업 구조 — 그리고 처음으로 NPU 가 CPU 보다 느린 silicon 결과를 정직하게 다룹니다.

모델

examples/silicon_llm/ — nanoGPT shakespeare_char.

구조     : Transformer decoder, character-level
파라미터 : 10,770,000 개  (D=384, n_head=6, n_layer=6, V=65, ctx=64)
학습     : RTX 3090, 5000 step → val 1.59 nats/char
가중치   : INT8 양자화, 10.7 MB → ARM ELF 에 번들
데이터셋 : TinyShakespeare

작지만 진짜 Transformer입니다 — attention, KV cache, FFN — GPT-4와 구조가 같고 파라미터 수만 다릅니다.

왜 L3가 아닌 L2 hybrid

XC7Z020 의 BRAM은 ~140 KB usable. nanoGPT 의 INT8 가중치는 6.9 MB. 못 들어갑니다. L3 (전체 그래프 단일 IP + 가중치 ROM) 는 가능하지 않음.

→ L2 hybrid 분업:

ARM (PS, 666 MHz)
  ├── token + positional embedding
  ├── causal attention + KV cache (가변 길이라 ARM 적합)
  ├── LayerNorm
  ├── LM head + argmax sampling

NPU (PL, 90 MHz) — llm_ffn_npu_top
  ├── FFN block: fc1[D→4D] → GELU → fc2[4D→D]
  ├── ap_start 1회 / layer = 6회 / token
  └── 가중치는 매 호출 DDR에서 m_axi 로 stream

Stateless IP — ARM이 매 호출 weight pointer + scale + 입력 활성을 register 로 넘김. PL fabric에 state 없음. 그래서 KV cache 와 context length 가 ARM-tunable, 재합성 없이 변경 가능.

silicon 결과 — 정확성

demo1: 5 prompt × CPU vs NPU, byte-equal 비교

[0] ROMEO:        CPU: '\nWhat is the wor'  NPU: '\nWhat is the wor'  → MATCH ✓
[1] JULIET:       CPU: '\nWhat is the wor'  NPU: '\nWhat is the wor'  → MATCH ✓
[2] HAMLET:       CPU: '\nWhat shall be s'  NPU: '\nWhat shall be s'  → MATCH ✓
[3] FIRST CITIZEN:CPU: '\nWhat is the wor'  NPU: '\nWhat is the wor'  → MATCH ✓
[4] MERCUTIO:     CPU: '\nThe shall be so'  NPU: '\nThe shall be so'  → MATCH ✓

CPU↔NPU 일치: 5/5    NPU FFN ap_starts: 708
RESULT: PASS

demo2: live 50자 셰익스피어 생성

ROMEO:
What is the world the world of the death?
ROMEO:

NPU 가 매 토큰 6번의 FFN MAC을 실 silicon DSP 에서 수행한 결과입니다.

silicon 결과 — 속도 (정직)

이게 P06/P07과 다른 부분입니다. NPU 가 CPU 보다 약 2.2× 느립니다.

wrap-safe demo3 per-phase 측정 (BENCHMARK.md):

componentCPU (ARM-VFP)NPU (PL FFN IP)비율
embed/ln/attn/lmhead/residual (ARM-only)동일동일1.00×
FFN block364 ms / token1022 ms / tokenNPU 2.81× 느림
per-token total549 ms / token1207 ms / tokenNPU 2.20× 느림

왜 NPU가 느린가 — 병목 분석

DMA 비용 분해 (per-forward, 6 layer 합산):

phaseper-forwardper-call% of FFN
dma_flush (cache flush)10.74 ms1.79 ms1.1%
dma_regwr (register 설정)0.02 ms0.00 ms0.0%
dma_poll (NPU 실 compute)1011 ms168 ms98.9%
dma_inv (cache invalidate)0.01 ms0.00 ms0.0%

98.9%가 IP 내부 compute 시간 입니다. 왜 느린가:

HLS C-synth는 FFN 1 호출당 ~55 ms를 예측했지만, silicon에서는 168 ms가 측정됩니다. 3× 차이. 원인은 6개의 m_axi 포인터가 단일 bundle=gmem을 공유하기 때문입니다. layer 당 ~1.5 MB INT8 가중치를 가져오는 burst read 가 직렬화됩니다.

XC7Z020 한계 매핑:

한계XC7Z020 값어떻게 보이나
BRAM 36k usable~140 KB6.9 MB 가중치 못 캐싱 → m_axi DDR 매번
m_axi HP0 bundle 공유64-bit, 1 bundle6 pointer 직렬화 = 핵심 병목
ARM L1 D-cache32 KB1.5 MB tile 못 들어감, 다만 cache-line streaming 이 contended m_axi 보다 효율적
PL Fmax91 MHz 달성HLS 예측 55 ms, 실측 168 ms

ARM 이 빠른 게 ARM 이 계산 강해서가 아니라, ARM 의 cache-line fill 패턴이 6개 contending m_axi master 보다 효율적이기 때문입니다.

3차례 결론 정정 (정직)

이 프로젝트는 정확한 측정에 도달하기까지 두 번 잘못된 결론을 발표했습니다. 모두 기록:

Round결론원인
1"NPU 1.63× 느림"uint32_t cyc_*_sum overflow ([demo1_verify.c:107-109])
2"NPU 5.1× 빠름"demo2 cycles_total (uint32_t) 가 50char × 1207ms = ~60s 실 wall 의 mod-2^32 wrap 읽어 Wall ms : 3338 stale 값
3 (current, correct)"NPU 2.20× 느림"wrap-safe demo3 per-phase 측정 — 각 PMU delta < 1 s, wrap 불가

PMU CCNT는 32-bit @ 666 MHz, 6.45초마다 wrap. 누적합 측정은 늘 위험. per-forward delta 만 신뢰 가능합니다.

속도를 높이려면

P08 BOARD_NOTES에 명시된 4가지 옵션:

  1. bundle=gmem 분리 — 4개 (fc1_W / fc2_W / fc1_b / fc2_b) 별도 m_axi master. 직렬화 제거. 가장 확실한 단일 변경.
  2. HP slave 128-bit — BD 에서 폭 늘림. IP-side 포트는 이미 지원.
  3. bigger D — MAC 수가 D² 로 증가, DMA 는 D — D ≥ 1024 에서 DMA tax amortise.
  4. ZU-class chip — 큰 BRAM (가중치 일부 캐싱), 넓은 m_axi, 빠른 PS (Cortex-A53 @ 1.5 GHz → ARM 측 비용 ~2× 단축).

두 silicon 함정 (X-1, X-2)

ap_done 이 이미 깨끗하게 raise 되는 상태에서만 보이는 함정:

X-1 — HLS register map hardcode 금지

ARM driver llm_npu.h 의 hardcoded offset이 HLS-generated xllm_*_hw.h 와 불일치. 원인: HLS는 scalar 인수를 pointer 사이에 inline 배치 (예: fc1_W_scale = 0x28, fc1_W = 0x1cfc1_b = 0x30 사이).

→ Fix: xhw.h에서 offset verbatim 복사. HLS/Vivado/Vitis 무수정, ARM .h 만.

X-2 — print_callback token ID raw cast

llm_generate(..., print_callback) 가 token ID (0..64) 를 (char)cast raw 로 callback 에 넘김. demo1 (array path) 에서는 caller 가 LLM_VOCAB_CHARS[id] lookup 으로 정상. demo2 (callback path) 에서는 ASCII garbage 출력.

→ Fix: callback 직전 LLM_VOCAB_CHARS[id] 변환.

상세: examples/silicon_llm/docs/BOARD_NOTES.md

재현 — 3 demo

# 보드 just-reset 상태
bash examples/silicon_llm/scripts/run_npu_test.sh demo3
python examples/silicon_llm/scripts/parse_benchmark.py
# → examples/silicon_llm/docs/results/BENCHMARK.md 자동 갱신
# 모두 (demo1 정확성 + demo2 live 생성 + demo3 wrap-safe benchmark)
bash examples/silicon_llm/scripts/run_npu_test.sh

이 데모가 증명하는 것 — 그리고 증명하지 않는 것

증명하는 것:

증명하지 않는 것:

자세한 자료

정리 — silicon ladder

단계패턴결과의미
08 P06 KWS L2/L3단일 IP, 가중치 BRAM ROMNPU 8.07× 빠름"FPGA NPU 가 의미 있다"
09 P07 Bearing같은 패턴, 더 깊은 그래프NPU 11.13× 빠름"같은 툴체인, 다른 모델"
[10 P08 LLM] (current)L2 hybrid, 가중치 DDRNPU 2.20× 느림 (정직)"LLM 도 올라간다, 속도는 정직히 측정"

FPGA NPU가 모든 경우에 빠른 것은 아닙니다. 다만 언제 빠르고 언제 느린지를 정직히 측정해 줍니다. 그게 다음 보드 선택과 NPU 설계 개선의 기준입니다.