‘AI PC’ 세대의 CPU에는 NPU (Neural Processing Unit)가 탑재되어 있습니다. 제가 사용하는 미니 PC(Beelink GTi14 Ultra / Intel Core Ultra 7 155H = Meteor Lake)에도 Intel AI Boost NPU가 장착되어 있지만, Proxmox 상의 컨테이너에서는 인식되지 않았습니다.
본 기사에서는 NPU를 비특권 LXC 컨테이너에 패스스루하여 다음 용도로 실제로 측정한 결과를 공유합니다.
A. embeddings (RAG 벡터화)
B. 음성 인식 (Whisper)
C. NPU와 내장 GPU 동시 실행
결론부터 말씀드리자면, Meteor Lake의 NPU는 ‘embeddings’와 ‘음성 인식’에서는 CPU보다 명확하게 빠르지만, LLM의 토큰 생성에서는 내장 GPU에 크게 뒤처진다는 결과였습니다.
관련 기사: 같은 머신의 내장 GPU로 Ollama / llama.cpp(Vulkan・SYCL・OpenVINO) / OVMS를 비교한 글도 작성했습니다. 본 기사는 그 ‘NPU 편’입니다.
| 용도 | NPU 결과 | CPU 대비 | :--- |
| embeddings (bge-small) | 126.9 texts/s (7.9 ms/text) | 약 2.4 배 |
| Whisper (base, 20초 음성) | 0.42s (RTF 0.021) | 약 1.5 배 |
| LLM 생성 (1.2B) | 9.5 tok/s | GPU의 1/4~1/5 |
| NPU + GPU 동시 | 합계 +10% (경합)
| — |
NPU의 실용적인 사용처는 embeddings와 음성 인식이었습니다.
| 항목 | 내용 |
|---|
| 머신 | Beelink GTi14 Ultra (DMI: AZW GTi14 ) |
| SoC | Intel Core Ultra 7 155H (Meteor Lake, 6P+8E+2LP-E = 16C/22T) |
| NPU | Intel AI Boost (NPU 3720, PCI 8086:7d1d ) |
| 내장 GPU | Intel Arc Graphics (8 Xe코어) |
| ... | 비특권 LXC (Ubuntu 24.04.4 LTS, 20GB RAM, 22 vCPU) |
| 컨테이너 | Docker (LXC 내에서 중첩 실행) |
| 유닛 | 개략 성능 |
|---|
| NPU (AI Boost) | 약 11 TOPS |
| ... | |
| NPU는 순수 처리량(raw throughput) 면에서는 내장 GPU보다 부족합니다. 장점은 저전력 소비(Intel 자료에 따르면 GPU의 절반 이하 전력 소모)이며, ‘GPU가 다른 작업을 수행하는 동안 상시 AI’ 작업에 적합합니다. | |
처음에는 일반적인 순서대로 lxc.cgroup2.devices.allow와 lxc.mount.entry를 수동으로 작성했을 때, 장치 노드는 보이지만 root 권한으로도 열 수 없는 (Permission denied) 상태가 되었습니다. 이는 장치의 소유자가 nobody:nogroup이 되어 장치 cgroup에 반영되지 않았기 때문입니다.
정답은 Proxmox의 **dev[n]: 단축키(shortcut)**를 사용하는 것입니다.
/dev/dri 패스스루와 같은 원리로, 노드 생성과 cgroup 허용을 한 번에 처리해 줍니다.
# /etc/pve/lxc/<CTID>.conf
dev0: /dev/dri/renderD128
dev1: /dev/dri/card0
...
비특권 LXC에서는 mknod가 Operation not permitted로 거부됩니다. 장치 노드를 직접 만들 수 없기 때문에, 반드시 호스트 측의 설정 변경이 필요합니다.
# 호스트(Proxmox)에서
ls -l /dev/accel/accel0 # 존재 확인 (261:0)
# /etc/pve/lxc/<CTID>.conf 에 dev2: /dev/accel/accel0 를 추가
...
재부팅 후, 게스트에서 /dev/accel/accel0가 root:root로 보이고 열 수 있게 됩니다.
커널 측에서는 intel_vpu가 로드되어 있어도, 사용자 공간의 Level Zero NPU 드라이버 (libze_intel_npu.so) 가 없으면 사용할 수 없습니다. 이번에는 OpenVINO 포함 이미지(후술)에 드라이버가 들어있었기 때문에 그것을 이용했습니다.
# 예: 이미지에서 드라이버를 추출
ls /usr/lib/x86_64-linux-gnu/libze_intel_npu.so*
모델의 OpenVINO IR 변환과 추론을 위해 Python 환경을 준비합니다. 함정이 많았기 때문에, 버전 고정이 중요합니다.
python3 -m venv /opt/ov-venv
/opt/ov-venv/bin/pip install torch --index-url https://download.pytorch.org/whl/cpu
/opt/ov-venv/bin/pip install openvino openvino-genai "optimum-intel[openvino]" \
...
주의사항:
torch와 torchvision의 버전이 어긋나면 operator torchvision::nms does not exist 오류로 인해 transformers의 import가 깨집니다. CPU 버전을 세트로 다시 설치해야 합니다. 또한, transformers는 5.4 이상 5.6 미만으로 고정합니다(후술).
bge-small-en-v1.5를 IR로 내보냅니다.
/opt/ov-venv/bin/optimum-cli export openvino \
--model BAAI/bge-small-en-v1.5 --task feature-extraction \
/opt/ov-models/bge-small-ov
내보낸 직후의 모델은 동적 쉐이프([?,?])로 되어 있어 NPU 컴파일러에 의해 거부됩니다 (INT64_MAX 차원 오류). 입력 길이를 고정하여 reshape 합니다.
import openvino as ov
core = ov.Core()
m = core.read_model("/opt/ov-models/bge-small-ov/openvino_model.xml")
...
측정 결과:
| 실행 위치 | 처리량 (Throughput) | 지연 시간 (Latency) |
|---|
| NPU | 126.9 texts/s | 7.9 ms/text |
| CPU | 52.4 texts/s | 19.1 ms/text |
NPU는 CPU의 약 2.4배입니다. 임베딩 일치도(cosine) 역시 0.99994로 실질적으로 동일한 출력을 얻었습니다. RAG의 벡터화 작업을 NPU에 맡기면, 내장 GPU를 LLM 용도로 비울 수 있습니다.
whisper-base를 내보냅니다. 여기가 가장 어려웠습니다.
/opt/ov-venv/bin/optimum-cli export openvino \
--model openai/whisper-base \
--task automatic-speech-recognition-with-past \
...
--task automatic-speech-recognition (without past)로 내보내면, decoder에 beam_idx가 포함되지 않아 WhisperPipeline이 Port for tensor name beam_idx was not found 오류로 실패합니다. -with-past를 붙이면 decoder에 beam_idx가 들어가 작동하게 됩니다. Whisper의 NPU 실행은, 내보낼 때 transformers >= 5.4가 사실상 필수입니다 (오래된 4.x로 내보낸 IR에는 NPU에서 invalid unordered_map key 오류를 일으키는 알려진 버그가 있습니다). transformers>=5.4,<5.6
에(に) 고정합시다. 실행 및 결과 (20.06초 음성, 3회 측정 최속값):
| 실행처 | 처리 시간 | RTF | 문자 기록 |
|---|
| NPU | 0.42s | 0.021 | CPU와 완벽 일치 |
| CPU | 0.65s | 0.033 | — |
NPU는 CPU의 약 1.5배 속도(20초 음성을 0.42초로 문자 기록). RTF 0.02는 '실시간의 1/50'으로, 상시 문자 기록에는 충분합니다.
import openvino_genai as ov_genai
pipe = ov_genai.WhisperPipeline("/opt/ov-models/whisper-past", "NPU", NPU_PLATFORM="3720")
print(pipe.generate(wav).texts[0])
'GPU로 LLM을 구동하면서, NPU로 다른 소형 모델을 구동하는' 구성을 검증했습니다. NPU에 1.2B, GPU에 4B를 올리고 동시에 요청을 보냅니다.
| 구성 | 처리량 (Throughput) |
|---|
| NPU 단독 (1.2B) | 8.7 tok/s |
| GPU 단독 (4B) | 13.9 tok/s |
| 동시 실행 합계 | 15.3 tok/s (NPU 6.5 + GPU 8.8) |
동시에 사용하면 각 스트림은 25~37% 저하하며, 총합도 GPU 단독 대비 +10% 정도에 그쳤습니다. 내장 GPU와 NPU가 메모리 대역폭이나 CPU를 공유하기 때문에, 경쟁(Contention)이 발생하고 있습니다.
→ 처리량을 높이는 목적보다는, 'GPU는 대화용으로 점유하고, NPU로 백그라운드 처리(embeddings・ASR)를 구동하는' 분리 용도가 현실적입니다.
같은 방식으로 1.2B의 LLM을 NPU에서 실행하면 9.5 tok/s였습니다. 내장 GPU의 Vulkan 실행 (같은 1.2B로 45.9 tok/s)의 약 1/5 수준이었습니다. 4B급은 NPU 그래프에 올라가지 않아 Compute error
으로 실패합니다.
NPU는 LLM 생성 속도 목적으로는 가치가 떨어집니다. 모델 대응 범위도 제한적입니다 (대칭 INT4 중심, 컨텍스트 상한이 작음).
| 용도 | NPU 평가 | 근거 |
|---|---|
| Embeddings / RAG | ◎ | CPU 대비 2.4배, 저전력 |
| ASR (Whisper) | ○ | CPU 대비 1.5배, 상시 처리 적합 |
| LLM 생성 | △ | GPU의 1/5 수준, 모델 대응도 제한적 |
| GPU와 동시 | △ | 총합 증가폭이 제한적 |
Meteor Lake의 NPU는 '임베딩'과 '음성 인식(ASR)'에 사용하는 것이 결론입니다. 둘 다 GPU에서 분리할 수 있기 때문에, 'GPU는 LLM, NPU는 RAG 벡터화와 문자 기록'이라는 역할 분담이 유효합니다.