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):
| component | CPU (ARM-VFP) | NPU (PL FFN IP) | 비율 |
|---|---|---|---|
| embed/ln/attn/lmhead/residual (ARM-only) | 동일 | 동일 | 1.00× |
| FFN block | 364 ms / token | 1022 ms / token | NPU 2.81× 느림 |
| per-token total | 549 ms / token | 1207 ms / token | NPU 2.20× 느림 |
왜 NPU가 느린가 — 병목 분석
DMA 비용 분해 (per-forward, 6 layer 합산):
| phase | per-forward | per-call | % of FFN |
|---|---|---|---|
| dma_flush (cache flush) | 10.74 ms | 1.79 ms | 1.1% |
| dma_regwr (register 설정) | 0.02 ms | 0.00 ms | 0.0% |
| dma_poll (NPU 실 compute) | 1011 ms | 168 ms | 98.9% |
| dma_inv (cache invalidate) | 0.01 ms | 0.00 ms | 0.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 KB | 6.9 MB 가중치 못 캐싱 → m_axi DDR 매번 |
| m_axi HP0 bundle 공유 | 64-bit, 1 bundle | 6 pointer 직렬화 = 핵심 병목 |
| ARM L1 D-cache | 32 KB | 1.5 MB tile 못 들어감, 다만 cache-line streaming 이 contended m_axi 보다 효율적 |
| PL Fmax | 91 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가지 옵션:
bundle=gmem분리 — 4개 (fc1_W / fc2_W / fc1_b / fc2_b) 별도 m_axi master. 직렬화 제거. 가장 확실한 단일 변경.- HP slave 128-bit — BD 에서 폭 늘림. IP-side 포트는 이미 지원.
- bigger D — MAC 수가 D² 로 증가, DMA 는 D — D ≥ 1024 에서 DMA tax amortise.
- 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 = 0x1c와 fc1_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
이 데모가 증명하는 것 — 그리고 증명하지 않는 것
증명하는 것:
- ✅ EulerNPU YAML → HLS → Vivado 툴체인으로 LLM도 FPGA NPU 에 올린다
- ✅ 작은 칩에 진짜 10.77M 파라미터 nanoGPT 가 들어간다
- ✅ Stateless IP + ARM KV cache + DDR weight streaming 패턴이 end-to-end 동작
- ✅ CPU↔NPU 5/5 byte-equal MATCH → NPU 수학 정확성
증명하지 않는 것:
- ❌ XC7Z020에서 NPU 가 ARM 보다 빠르다 — 사실 2.2× 느림 (정직히)
- ❌ 다른 보드에서도 같은 결과 —
bundle=gmem분리 + ZU-class 로 가면 결과 다름 - ❌ greedy decoding 의 텍스트 품질 — val 1.59 nats/char + argmax 이라 반복 발생
자세한 자료
- examples/silicon_llm/docs/tutorial.md — 전체 흐름
- examples/silicon_llm/docs/results/SUMMARY.md — 측정값 + 3차 정정 기록
- examples/silicon_llm/docs/results/BENCHMARK.md — wrap-safe 컴포넌트별 표
- examples/silicon_llm/docs/BOARD_NOTES.md — X-1 ~ X-5 함정
정리 — silicon ladder
| 단계 | 패턴 | 결과 | 의미 |
|---|---|---|---|
| 08 P06 KWS L2/L3 | 단일 IP, 가중치 BRAM ROM | NPU 8.07× 빠름 | "FPGA NPU 가 의미 있다" |
| 09 P07 Bearing | 같은 패턴, 더 깊은 그래프 | NPU 11.13× 빠름 | "같은 툴체인, 다른 모델" |
| [10 P08 LLM] (current) | L2 hybrid, 가중치 DDR | NPU 2.20× 느림 (정직) | "LLM 도 올라간다, 속도는 정직히 측정" |
FPGA NPU가 모든 경우에 빠른 것은 아닙니다. 다만 언제 빠르고 언제 느린지를 정직히 측정해 줍니다. 그게 다음 보드 선택과 NPU 설계 개선의 기준입니다.