프레임워크 오버헤드를 우회하기 위해 Orange Pi AIPro (Ascend 310B)에서 MiniCPM-V 4.6을 위한 커스텀 C++
요약
Orange Pi AIPro(Ascend 310B) 환경에서 MiniCPM-V 4.6을 구동하기 위해 프레임워크 오버헤드를 제거한 커스텀 C++ 추론 엔진을 개발했습니다. AscendC 커널 최적화를 통해 기존 베이스라인 대비 추론 속도를 약 2배 향상시켰습니다.
핵심 포인트
- Ascend 310B NPU를 위한 로우 레벨 C++ 추론 엔진 구축
- 커스텀 큐브 행렬 곱 커널로 M=1 상황의 성능 최적화
- 대규모 어휘 사전 대응을 위한 가중치 슬라이싱 기법 적용
- FP16 기준 2.88 tps에서 5.90 tps로 성능 2배 향상
안녕하세요 여러분, 지난 몇 주 동안 제가 작업해 온 프로젝트를 공유하고자 합니다. 저는 MiniCPM-V 4.6을 Orange Pi AIPro(Ascend 310B NPU가 탑재된 가성비 보드로, 20 TOPS INT8 / 10 TFLOPS FP16 성능에 약 149달러 정도 합니다)에서 완전히 실행할 수 있는 바닥부터 만든(from-scratch) C++ 추론 엔진(inference engine)을 구축하는 데 성공했습니다.
커스텀 연산(custom ops), 빌드 스크립트 또는 Gradio 웹 UI를 확인하고 싶으시다면, 해당 리포지토리는 GitHub의 github.com/lvyufeng/minicpm-v-4.6-orangepi에 오픈 소스로 공개되어 있습니다.
이 특정 하드웨어에서 로컬 LLM 또는 VLM을 배포하려고 시도해 본 적이 있다면, 특히 엣지(edge)에서 괜찮은 성능을 얻고자 할 때 표준 프레임워크 스택(framework stack)을 다루는 것이 얼마나 큰 고역인지 아마 알고 계실 것입니다. 이를 해결하기 위해 저는 무거운 프레임워크들을 건너뛰고 로우 레벨(low-level)로 내려갔습니다. 텍스트 생성(text generation)과 SigLIP 비전 타워(vision tower) 모두 단일 C++ 서브프로세스 내에서 NPU 상에서 네이티브로 실행됩니다. 핫 패스(hot path) 상에서는 torch_npu 의존성이 전혀 없습니다. Python은 CPU 측의 토큰화(tokenization) 및 이미지 전처리(image preprocessing)를 위한 콜드 패스(cold path)에서만 사용됩니다.
초기 기본값인 aclnnMm 베이스라인은 M=1(벡터-행렬 곱, vector-matrix multiply)일 때 NPU의 큐브 유닛(cube unit)을 심하게 활용하지 못했기 때문에 토큰 디코딩(token decoding) 단계에서 꽤 형편없었습니다. 당시에는 초당 약 2.88 토큰(step당 약 350ms 소요)을 생성했습니다.
커스텀 AscendC 커널(kernels)로 크리티컬 패스(critical paths)를 다시 작성한 후, 현재 FP16에서 5.90 tokens/s를 달성했습니다(step당 지연 시간(latency)을 170ms로 단축). 다음은 2배의 속도 향상이 어떻게 일어났는지에 대한 실제 세부 내역입니다:
| 단계 | Tokens/s | Step당 (ms) | 절감된 시간 |
|---|---|---|---|
기본 aclnnMm 베이스라인 | 2.88 | 350 ms | — |
| ... |
먼저, 느린 범용 벡터 경로(generic vector path)를 우회하기 위해 MatmulImpl을 사용하여 M=1인 경우를 위한 커스텀 큐브 행렬 곱(cube matmul) 커널을 작성했습니다. 이 단일 변경만으로 속도가 2.88 tps에서 4.37 tokens/s로 향상되었으며, step당 약 121ms를 절감했습니다.
둘째, 어휘 사전 크기(vocabulary size)가 매우 크기 때문에(약 248k), lm_head가 일반적인 큐브 타일링(cube tiling)을 수행하기에는 너무 넓었습니다. 기본 matmul(행렬 곱셈)을 직접 실행하는 것이 병목 현상(bottleneck)이었습니다. 그래서 저는 엔진이 로드 시점에 가중치를 큐브 친화적인 16개의 슬라이스(slice)로 분할하여, 순차적인 matmul을 실행한 뒤 호스트 리듀스(host reduce)를 수행하도록 만들었습니다. 이를 통해 29ms를 추가로 단축하여 4.99 tokens/s까지 끌어올렸습니다.
셋째, 매우 스칼라(scalar) 방식이었던 causal-conv1d 베이스라인을 Unified Buffer DMA를 사용하는 벡터화된 스텝 커널(vectorized step kernel)로 교체했습니다. 이를 통해 스텝당 30ms를 추가로 절약하여 최종적으로 5.90 tokens/s에 도달했습니다.
현재 디코딩 스텝은 FP16 가중치를 읽어들이는 보드의 44 GB/s 메모리 대역폭(memory bandwidth)에 의해 완전히 병목이 걸려 있는 상태입니다. 스텝당 1.4GB의 가중치를 읽는 데 필요한 절대적인 이론적 최솟값은 약 32ms이며, 현재 저의 큐브 경로(cube path)는 170ms에 머물러 있습니다. 다음 논리적인 단계는 큐브 경로에 융합된(fused) INT4/INT8 역양자화(dequantization) 커널을 구현하여 12+ tokens/s 이상으로 밀어붙이는 것입니다.
AscendC 커널 튜닝, C++ SigLIP 구현, 또는 일반적인 엣지 VLM 배포에 대해 궁금한 점이 있다면 언제든 말씀해 주세요!
AI 자동 생성 콘텐츠
본 콘텐츠는 Reddit AI Engineering의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기