Projekty i notatki

W laptopie mam nieużywane NPU. Chcę wreszcie ustalić, czy może mi się do czegoś przydać

  • LLM
  • Snapdragon
  • NPU

W swoim laptopie Lenovo Yoga Slim 7x mam oprócz standardowego CPU i GPU jeszcze jeden procesor: NPU (Neural Processing Unit), przeznaczony do energooszczędnego wykonywania obliczeń sieci neuronowych. Na co dzień ten NPU, może poza rozmywaniem tła podczas rozmowy czy indeksowaniem plików w tle, pozostaje bezczynny. Postanowiłem więc sprawdzić, co jeszcze da się z nim zrobić, jaką oferuje wydajność i czy w praktyce warto z niego korzystać.

Zacząłem od sprawdzenia, jakie modele językowe można na nim uruchomić, przez jakie narzędzia, ile zachodu wymaga przygotowanie i czy rezultat jest wart tego wysiłku. Zmierzyłem szybkość generowania, czas oczekiwania na odpowiedź oraz zachowanie przy różnych długościach wejścia i stanach zasilania. Sprawdziłem też wybrane rozmiary modeli i kwantyzacje; nie uzyskałem wiarygodnego pomiaru zużycia RAM.

Mam nadzieję, że lokalny LLM przyda się tam, gdzie połączenie z chmurą jest słabe albo nie ma go wcale, na przykład w samolocie lub w pociągu. Być może wystarczy do pomocy przy tekście, kodzie lub notatkach zapisanych na laptopie. Warto też wiedzieć, jakie zadania da się wykonać bez przesyłania promptu do zewnętrznego modelu. W takich warunkach NPU może okazać się szczególnie przydatne: powstało właśnie po to, by obliczenia sieci neuronowych wykonywać oszczędniej niż CPU i GPU, a ASUS wprost łączy je z niskim zużyciem energii i dłuższą pracą na baterii (ASUS). Sprawdziłem więc szybkość i przybliżoną moc działających konfiguracji; czasu pracy na baterii nie mierzyłem.

Nie zakładałem, że lokalny model zastąpi chmurę ani że NPU musi wygrać z CPU lub GPU. Celem było ustalenie, kiedy warto użyć NPU, którą ścieżkę wybrać i gdzie kończy się jego praktyczna użyteczność. Przewaga CPU lub GPU w konkretnym scenariuszu była równie wartościowym wynikiem.

Dodatkową motywacją jest dla mnie chęć lepszego poznania technologii i chęć poeksperymentowania z parametrami modeli.

Punktem startowym był Qwen3-4B-Instruct-2507. Następnie sprawdziłem wybrane rozmiary modeli, kwantyzacje, pojemności okna i rzeczywiste długości wejścia. Porównania dotyczą natywnego Windows ARM64 na tym laptopie.

Najważniejsze wyniki:

  • Krótkie wejście, zasilacz: CPU z Q4_0 daje około 33 tok/s — najszybciej.
  • Bateria + Power Saving: NPU (qairt) utrzymało około 20 tok/s — najwyższe tempo decode spośród badanych konfiguracji w tym trybie.
  • Długie wejście (6180 tokenów): generacja około 33 s na NPU wobec około 86 s na CPU.
  • Rozmiar modelu: 8B i 14B osiągnęły tempo użyteczne do interaktywnej pracy; 32B (około 4,8 tok/s) było dla mnie zbyt wolne do wygodnego czatu.
  • NPU nie jest automatycznie najlepszym wyborem — zależy od scenariusza.

Odczyty mocy na poniższym wykresie to przybliżenia netto kanału SYS z osobnych, pojedynczych prób, z odpowiedziami różnej długości i treści. Dokładny zakres licznika i błąd pomiaru nie są ustalone. Nie są to pomiary czasu pracy na baterii ani energii jednakowego zadania.

Szybkość a średnia moc
Mediana decode z 5 prób; moc to pojedynczy odczyt netto kanału SYS (po odjęciu spoczynku). CPU z Power Saving nie ukończył próby.
Dłuższe wejście: CPU czy NPU?
128 tokenów odpowiedzi · mniej = lepiej
Mediany z 3 mierzonych prób na punkt. Qwen3-4B-Instruct-2507 Q4_0, stałe okno 16 384 tokenów, zasilacz i maksymalna wydajność. Czas obejmuje przetworzenie wejścia i wygenerowanie odpowiedzi, bez ładowania modelu. NPU przez ggml-hexagon (GGUF), nie QNN. Linie łączą trzy zmierzone długości wejścia; nie wyznaczają dokładnego punktu zmiany przewagi.

Dłuższe wejście może zmienić wybór urządzenia. Przy 420 tokenach wejścia CPU kończył generację wcześniej: 6,69 s wobec 9,31 s na NPU. Przy 6180 tokenach NPU potrzebowało 32,96 s, a CPU 86,03 s — w tym teście NPU było około 2,6× szybsze.

1. Podstawowe informacje

1.1. Sprzęt

Testuję Yoga Slim 7x ze Snapdragonem X Elite X1E-78-100 i 32 GB RAM. W jednym SOC są trzy urządzenia, na których można wykonywać obliczenia modelu.

Laptop wyposażony jest w procesor Qualcomm Snapdragon X Elite z trzema jednostkami obliczeniowymi:

  • CPU - 12 rdzeni Qualcomm Oryon, podzielonych na trzy klastry po cztery rdzenie, taktowanie do 3,4 GHz przy pracy wielordzeniowej.
  • GPU - zintegrowany Qualcomm Adreno X1-85, korzystający ze współdzielonej pamięci RAM, z obsługą OpenCL.
  • NPU - Qualcomm Hexagon o deklarowanej wydajności do 45 TOPS (bilionów operacji na sekundę).

TFLOPS i TOPS nie da się łatwo przeliczyć na tokeny/s. Szybkość generowania zależy też od modelu, przesyłania danych z pamięci i implementacji obliczeń. Trzeba ją zmierzyć dla konkretnej konfiguracji.

1.2. Modele i pliki

Wykorzystane modele

W testach wykorzystałem głównie Qwen3-4B-Instruct-2507. Oznaczenie 4B w jego nazwie oznacza około czterech miliardów parametrów. Ta wersja modelu działa wyłącznie w trybie non-thinking. Mniejszy model nie jest po prostu mocniej skwantyzowanym wariantem większego. Ma inną liczbę parametrów, a poszczególne modele mogą różnić się również architekturą i treningiem.

Dla porównania zmierzyłem też Qwen3-1.7B, 8B, 14B i 32B na CPU z wyłączonym trybem thinking.

Podjąłem również próbę uruchomienia Qwen3-235B-A22B Q4_K_M, modelu typu MoE (Mixture of Experts). W tej architekturze router wybiera część ekspertów do przetwarzania danego tokenu. Oznaczenie 235B-A22B mówi o około 235 miliardach parametrów łącznie i około 22 miliardach aktywnych przy przetwarzaniu tokenu. Nie oznacza to jednak, że wystarczy pamięć na wagi modelu 22B. W mojej próbie model nie przeszedł etapu ładowania, więc nie mam dla niego wyniku szybkości.

Formaty plików modeli

Modele zapisywane są w różnych formatach, które różnią się zawartością plików i ich ilością nawet jeśli sam model jest ten sam. Dlatego ten sam model, np. Qwen, można pobrać w kilku wersjach. Poniżej opisuję najważniejsze formaty i różnice między nimi.

Pliki wag PyTorch

W starszych repozytoriach na Hugging Face często spotkamy pliki takie jak pytorch_model.bin (a także z rozszerzeniami .pt i .pth), zapisane mechanizmem serializacji PyTorcha. Wykorzystuje on pod spodem pythonowy moduł pickle. Z powodu możliwości egzekucji złośliwego kodu (ACE) format ten jest obecnie wypierany przez bezpieczniejsze alternatywy.

Safetensors

Format powstał jako bezpośrednia odpowiedź na luki w plikach .bin. Plik .safetensors to płaski bufor binarny zawierający wyłącznie nazwy tensorów, ich typy, wymiary i wartości. Dobrze nadaje się do bezpiecznego udostępniania wag modelu, ale nie zapisuje kompletnego przepisu na architekturę obliczeń modelu.

GGUF

GGML to biblioteka obliczeń tensorowych używana m.in. przez llama.cpp. GGUF jest formatem zapisu modeli z tego ekosystemu. GGUF może zawierać nie tylko wagi (w tym skwantyzowane), ale też rozszerzalne metadane: nazwę architektury, jej parametry, tokenizer i szablon rozmowy.

Konteksty QNN i DLC

Zamiast przekazywać programowi same wagi, można wcześniej przygotować obliczenia pod akcelerator. Narzędzia Qualcomma przyjmują opis sieci i jej wagi, przekształcają obliczenia do postaci obsługiwanej przez wybrany backend i pozwalają zapisać rezultat. Taki zapis to kontekst QNN.

W paczkach dla układów Hexagon kontekst jest często podzielony na pliki .bin (uwaga: to zupełnie inny format niż opisany wcześniej .bin PyTorcha!) .DLC (Deep Learning Container) to z kolei dedykowany kontener Qualcomma do zapisania sieci, który również może zawierać przygotowany kontekst.

Skąd program wie, jak z tych plików zbudować sieć?

Mechanika budowania sieci zależy od formatu oraz ekosystemu, w którym dany model uruchamiamy:

  • Hugging Face (Safetensors / PyTorch): Pliki wag dostarczają tylko liczby. W typowym repozytorium obok wag znajduje się plik config.json, który określa typ architektury i jej parametry (np. liczbę warstw i rozmiary wektorów). Kod wykonujący tę architekturę znajduje się zwykle w zewnętrznej bibliotece transformers. Klasa taka jak AutoModelForCausalLM odczytuje konfigurację (wskazującą np. na Qwen3ForCausalLM), tworzy szkielet sieci i dopiero wtedy wczytuje do niego wagi. Tokenizer dostarczany jest jako osobne pliki.
  • GGML (GGUF): Kod architektury nie jest pobierany z biblioteki Transformers, lecz jest wbudowany bezpośrednio w program (np. llama.cpp). Program odczytuje z pliku GGUF, że ma uruchomić np. architekturę Qwen3, pobiera zaszyte w kontenerze ustawienia sieci i używa własnej, wewnętrznej implementacji do ułożenia obliczeń (implementacja Qwen3 w llama.cpp). Dlatego nowa architektura wymaga aktualizacji kodu samego programu, a nie tylko konwersji wag do formatu GGUF.
  • Qualcomm (QNN/DLC): Program ładuje gotowe, prekompilowane instrukcje dla akceleratora przez interfejs wykonawczy QNN. Nie musi budować architektury warstwa po warstwie i mapować do nich wag tak jak robi to Transformers czy llama.cpp. Wymaga to zazwyczaj załadowania gotowych kontekstów .bin, konfiguracji paczki i tokenizera.
ONNX: graf obliczeń i jego wykonanie

ONNX, czyli Open Neural Network Exchange, powstał jako wspólna reprezentacja modeli, umożliwiająca przenoszenie ich między narzędziami. Nie jest biblioteką liczącą model. Opisuje obliczenia niezależnie od konkretnej implementacji sprzętowej.

W modelu ONNX znajdziemy:

  • operacje i połączenia między nimi,
  • wejścia i wyjścia, ich typy i wymiary oraz wersje używanych zestawów operatorów,
  • wagi i stałe lub odwołania do osobnych plików danych.

Na prostym przykładzie:

x → MatMul z W → Add z b → Relu → y

Safetensors może zapisać W i b. Plik ONNX zawiera strukturę całego grafu, wejścia/wyjścia, oraz same wagi. Runtime nadal musi wiedzieć, jak wykonać MatMul, Add i Relu.

ONNX to opis grafu, na podstawie którego runtime przygotowuje wykonanie. W ONNX Runtime wygląda to w uproszczeniu tak:

  1. Optymalizacja: ORT wczytuje graf i na poziomie oprogramowania upraszcza go – np. pre-kalkuluje stałe fragmenty lub łączy kilka sąsiadujących operacji w jedną szybszą.
  2. Dzieli pracę między Execution Providers, czyli integracje z backendami i akceleratorami. Np. na Snapdragonie interesuje mnie QNN Execution Provider.
  3. Provider przygotowuje obsługiwane fragmenty dla swojego backendu, a runtime organizuje ich wykonanie. Operacje mogą być realizowane przez gotowe kernele lub przygotowany silnik, nie przez bezpośrednie wykonywanie instrukcji ONNX.

Nieobsługiwane operacje mogą trafić na CPU, jeśli konfiguracja dopuszcza fallback, albo uruchomienie zostanie odrzucone.

Podsumowanie formatów

Plik/format Zawartość Typowe oprogramowanie Gdzie są wykonywane obliczenia?
.safetensors Nazwane tensory, ich typy, wymiary i wartości. Także reprezentacje skwantyzowane, jeśli obsługuje je użyte narzędzie PyTorch + Transformers (biblioteka zewnętrzna dostarcza kod architektury). Na urządzeniach obsługiwanych przez dany stos. Sam format nie zapewnia ścieżki NPU na mojej Yodze
pytorch_model.bin Zrzut obiektów z pamięci przez mechanizm serializacji PyTorcha (pickle) PyTorch lub biblioteka korzystająca z niego Zależnie od implementacji i backendów PyTorcha
.gguf Tensory i metadane modelu, opcjonalnie tokenizer i szablon rozmowy llama.cpp CPU, GPU lub NPU, jeśli build i backend obsługują model oraz typy tensorów
.onnx Niezależny od sprzętu graf operacji oraz powiązane z nim wagi ONNX Runtime (ORT). CPU, GPU lub NPU przez odpowiedni Execution Provider. Nie każdy graf działa na każdym urządzeniu
Kontekst QNN .bin Zamknięte, skompilowane instrukcje dla konkretnego sprzętu Interfejs QNN (np. z pluginem qairt). W omawianej ścieżce backend HTP i Hexagon NPU
.dlc Deep Learning Container – specyficzny kontener Qualcomma na sieć i ew. przygotowany kontekst. Zgodne narzędzia i runtime Qualcomma Wymaga ścisłej zgodności pliku z docelowym układem SoC i wersją oprogramowania.

1.3. Czym uruchomić model na CPU, GPU i NPU?

W celu uruchomienia modelu na konkretnym hardware potrzebny jest runtime, który załaduje model i zorganizuje wykonanie, oraz backend obliczeniowy, który wykona obsługiwane operacje na danym urządzeniu.

llama.cpp

Jest to runtime odczytujący format GGUF. Implementuje on obsługiwane architektury modeli i zleca pracę backendom obliczeniowym. Na tej platformie interesują mnie trzy z nich: CPU, OpenCL (dla GPU Adreno) oraz eksperymentalny ggml-hexagon (dla NPU Hexagon).

Ekosystem Qualcomma posługuje się własną terminologią dla poszczególnych warstw stosu:

Nazwa Rola
QNN Niskopoziomowe API i warstwa backendów do kompilacji oraz wykonania grafów.
QNN HTP backend Backend QNN kierujący obsługiwane obliczenia bezpośrednio do Hexagona. HTP to Hexagon Tensor Processor.
QAIRT Szerszy stos SDK i narzędzi wykonawczych Qualcomma.
Genie Wewnętrzny framework generatywny w ekosystemie Qualcomma.
GenieX Narzędzia i SDK udostępniające elastyczne ścieżki wykonania (m.in. przez system pluginów).
ONNX Runtime GenAI Wyższa warstwa oparta na ONNX Runtime, obsługująca m.in. pętlę generowania tokenów, sampling i KV Cache.

GenieX

To udostępnione przez Qualcomma narzędzie do uruchamiania modeli, działające w oparciu o pluginy. Wykorzystuje m.in. llama_cpp dla plików GGUF oraz qairt dla przygotowanych paczek QNN. Obie te drogi zmierzyłem. Qualcomm określa GenieX jako społecznościową wersję Genie, ale nie są to nazwy zamienne. Genie obsługuje generowanie w innych integracjach, np. w ścieżce modeli NPU w AnythingLLM (gdzie aplikacja pobiera paczkę z konfiguracją Genie i prekompilowanymi plikami QNN).

Próby szybkości wykonałem przez GenieX. Choć podczas przygotowań sprawdzałem również samodzielny build llama.cpp, podane w dalszej części wyniki pochodzą z GenieX. Integracji Genie wewnątrz ORT nie mierzyłem.

Podsumowanie ścieżek wykonania i dróg do sprzętu:

Wersja artefaktu Oprogramowanie / integracja Droga do urządzenia
GGUF Samodzielne llama.cpp lub GenieX z pluginem llama_cpp Backend CPU → rdzenie Oryon
GGUF Samodzielne llama.cpp lub GenieX z pluginem llama_cpp Backend OpenCL → GPU Adreno
GGUF Samodzielne llama.cpp lub GenieX z pluginem llama_cpp ggml-hexagon → NPU Hexagon
Paczka kontekstów QNN GenieX z pluginem qairt QNN → backend HTP → NPU Hexagon
Przygotowany model ONNX dla QNN ONNX Runtime GenAI z QNN EP QNN → backend HTP → NPU Hexagon
Paczka zgodna ze ścieżką Genie w ORT ORT GenAI z backendem Genie Stos Qualcomma → NPU Hexagon

Powyższe ścieżki zostały szerzej opisane w dokumentacji GenieX oraz przewodniku ORT GenAI po QNN i Genie.

Uruchomienie modelu GGUF na Hexagonie nie wymaga uprzedniej konwersji do ONNX ani przechodzenia przez QNN. Z kolei fakt, że dwie inne ścieżki wykorzystują wspólne QNN, nie gwarantuje identycznej szybkości ani zamienności paczek.

CPU steruje układem NPU, organizuje tokenizację tekstu i odpowiada za sampling. Jest to przewidywane i nie jest to to samo, co fallback obliczeń zrzuconych awaryjnie z akceleratora.

1.4. Kwantyzacja

Kwantyzacja przybliża wartości liczbowe, używając ograniczonej reprezentacji. Mniej bitów może oznaczać mniejszy plik, mniejszy koszt pamięci i mniej danych do przesłania. Nie gwarantuje przyspieszenia, bo backend musi mieć wydajną implementację danego schematu. Może też zmienić odpowiedzi modelu.

Oznaczenie Co w rzeczywistości oznacza?
BF16 / F16 Brak kwantyzacji (16-bit). Pełne wagi. Zapis BF16 zachowuje szerszy zakres liczb kosztem precyzji ułamków, a F16 odwrotnie.
Q8_0 Kwantyzacja blokowa w GGML. Przybliżenia wag są zapisywane jako 8-bitowe kody ze wspólną skalą dla bloku. Nie jest to proste obcięcie oryginalnego zapisu każdej wagi do 8 bitów.
Q4_0 Kwantyzacja blokowa w GGML. Przybliżenia wag są zapisywane jako 4-bitowe kody ze wspólną skalą dla bloku. Zmniejsza rozmiar reprezentacji i może zmienić jakość odpowiedzi; wpływu na jakość nie mierzyłem w tych testach.
Q4_K_M Mieszany wariant kwantyzacji w GGML. Łączy Q4_K z wyższą precyzją wybranych tensorów, m.in. Q6_K. Może oferować inny kompromis rozmiaru, szybkości i jakości niż Q4_0, ale nie ustaliłem tu najlepszego wariantu pod względem jakości.
W4A16
(w QNN)
Etykieta precyzji w recepturze Qualcomma. Nominalnie oznacza wagi INT4 oraz aktywacje INT16. Nie jest formatem pliku ani bezpośrednim odpowiednikiem schematu Q4_0 z GGML.

Nie oceniałem jakości odpowiedzi, więc te oznaczenia i wyniki szybkości nie wyznaczają rankingu jakości ani optymalnego kompromisu rozmiaru i jakości.

Co w praktyce oznacza kwantyzacja „4-bitowa”?

To nie jest tak, że każda oryginalna waga zapisywana jest jako osobna, precyzyjna liczba zajmująca równo 4 bity. Kwantyzacja to kompresja blokowa – zapisujemy jedynie przybliżenie wagi za pomocą krótkiego kodu i mnożnika (skali), który pozwala odtworzyć jej orientacyjną wartość w locie. Same 4 bity dają zaledwie 16 możliwych stanów.

W schemacie Q4_0 wagi grupowane są po 32 sztuki. Taki blok dzieli ze sobą jedną wspólną, 16-bitową skalę:

Struktura pojedynczego bloku Q4_0:
[ skala: 2 bajty ] + [ 32 kody po 4 bity: 16 bajtów ]

Razem: 18 bajtów na 32 wagi. 
Daje to średnio 4,5 bitu na każdy parametr modelu.

Skala i kody muszą pracować razem. Układ „Q4” opisuje konkretną strukturę blokową, a nie idealne matematyczne 4 bity pamięci na każdą pojedynczą wagę.

W4A16: wagi to nie wszystkie liczby w sieci

Podczas działania (inferencji) w pamięci operacyjnej pojawiają się dwa różne typy danych:

  • Wagi (Weights - W): wyuczone parametry modelu. Są statyczne i podczas generowania tekstu tylko je odczytujemy.
  • Aktywacje (Activations - A): tymczasowe wyniki obliczeń przekazywane między warstwami. Zmieniają się dynamicznie dla każdego przetwarzanego wejścia.

Zasada działania wygląda w uproszczeniu tak: aktywacje wejściowe × wagi → aktywacje wyjściowe.

W4A16 to etykieta precyzji, a nie format pliku. W recepturze Qualcomma oznacza to nominalnie: wagi jako liczby całkowite INT4 oraz aktywacje liczone w INT16 (a nie zmiennoprzecinkowym FP16). Etykieta ta określa pożądaną matematykę, ale nie definiuje samego układu bloków (jak robi to Q4_0), precyzji KV Cache, ani sposobu sumowania sprzętowego w NPU. Choć Q4_0 i W4A16 mają w nazwie czwórkę, oznaczają inną kwantyzację i inną warstwę abstrakcji.

Co właściwie porównuję?

Sprawdzam, co warto uruchamiać na CPU, GPU i NPU tego laptopa, z jaką kwantyzacją i w jakich warunkach. NPU było punktem wyjścia, ale nie zakładam, że będzie najlepszym wyborem. Pomiary wykonałem przez GenieX:

Ścieżka w GenieX Urządzenie lub tryb Sprawdzone warianty 4B
llama_cpp CPU GGUF: F16, Q8_0, Q4_K_M, Q4_0
llama_cpp + OpenCL GPU Adreno GGUF Q4_0
llama_cpp + ggml-hexagon NPU Hexagon GGUF: F16, Q8_0, Q4_K_M, Q4_0
llama_cpp, tryb hybrid Tryb mieszany, bez zweryfikowanego podziału pracy GGUF Q4_0
qairt + QNN HTP NPU Hexagon Przygotowana paczka QNN W4A16

Osobno sprawdziłem modele od 1.7B do 32B na CPU, większą pojemność okna i dłuższe wejście. Porównałem też zachowanie na zasilaczu, baterii i z Power Saving.

Przy porównaniu tego samego GGUF między urządzeniami zachowuję ten sam plik modelu. Przejście na paczkę QNN zmienia także kwantyzację i runtime, więc ten wynik porównuje całe konfiguracje, nie wyłącznie sprzęt.

1.5. Jak czytać czas, kontekst i pamięć

  • Prefill: przetworzenie wejścia.
  • TTFT: czas od rozpoczęcia żądania do pierwszego tokenu odpowiedzi.
  • Decode: tempo generowania kolejnych tokenów, podawane w tok/s.

Raportowane TTFT i czasy generacji nie obejmują ładowania modelu. Krótszy TTFT nie musi oznaczać szybszego ukończenia całej odpowiedzi.

Pojemność okna to nie długość wejścia. Wejście, historia, znaczniki rozmowy i odpowiedź dzielą budżet kontekstu. KV cache przechowuje część obliczeń dla zachowanych tokenów, ale dłuższa historia nadal zwiększa koszt pamięci i uwagi. Dlatego osobno zmieniam pojemność okna, a osobno faktyczną długość promptu.

Rozmiar pliku modelu nie jest równoznaczny ze zużyciem RAM. Do wag dochodzą cache, aktywacje, bufory i pamięć systemu. Nie uzyskałem wiarygodnego pomiaru RAM.

2. Co rzeczywiście udało się uruchomić

Najpierw zweryfikowałem, czy dane środowisko jest w ogóle w stanie zainicjować model i wygenerować odpowiedź. Do finalnych pomiarów wydajności trafiły wyłącznie stabilne, w pełni działające konfiguracje.

2.1. Bilans środowisk

Środowisko Wynik lokalnej próby uruchomienia Czy podaję benchmark?
Samodzielne llama.cpp (ARM64) CPU działa, OpenCL poprawnie wykrywa Adreno. Build z ggml-hexagon wykrywa NPU, ale nie otwiera sesji. Nie
GenieX + llama_cpp Generowanie działa poprawnie we wszystkich trybach: CPU, GPU, NPU i hybrid. Tak
GenieX + qairt Prekompilowana paczka QNN (W4A16) poprawnie ładuje się i działa na NPU. Tak
ONNX Runtime GenAI Potencjalna alternatywa, jednak bez pomyślnie zakończonej, lokalnej próby generowania. Nie

Przebieg i problemy z uruchamianiem

Samodzielne llama.cpp Kompilacja wersji ARM64 dla bazowego CPU zakończyła się sukcesem i działa stabilnie. Wariant z włączonym backendem ggml-hexagon wykrywa NPU, ale próba załadowania modelu kończy się błędem otwarcia sesji. Przyczyna błędu pozostaje nieznana, ale nie jest to dowód na brak sprzętowej obsługi tego modelu – ta sama ścieżka obliczeniowa zadziałała bezbłędnie przez biblioteki llama.cpp dostarczane wewnątrz GenieX.

GenieX z pluginem llama_cpp (GGUF) Zarejestrowałem lokalny plik GGUF i wykonałem próbną inicjalizację NPU. Właściwe pomiary wydajności objęły tryby: CPU, GPU, NPU oraz hybrid. W trybie hybrid rzeczywisty podział pracy między bloki SoC pozostaje z perspektywy użytkownika czarną skrzynką.

GenieX z pluginem qairt (Paczka QNN) Do testów użyłem przygotowanej paczki QNN W4A16 dla układów Snapdragon X Elite, zgodnej z pluginem qairt.

2.2. Czego nie mierzyłem

ORT rozpatrywałem jako alternatywę, ale jego sprawdzenie wymagałoby przygotowania lub pozyskania zgodnego modelu, konfiguracji generowania i zweryfikowania działania wybranej integracji. Byłby to osobny etap prac, wymagający dodatkowego czasu. Ograniczyłem więc zakres do działających konfiguracji GenieX. Brak wyników ORT jest decyzją o zakresie testów, nie dowodem, że ta droga nie działa.

3. Jak mierzę i co porównuję

Testowałem GenieX v0.10.0 z QAIRT 2.50 i dostarczoną z nim wersją llama.cpp.

Pomiary szybkości wykonałem natywnie na Windows ARM64 przez GenieX. Dla tabel szybkości i kwantyzacji każda ukończona komórka to jedna próba rozgrzewki i pięć mierzonych. Podaję medianę. Testy pojemności okna i długości wejścia mają po trzy mierzone próby. Ustawienia to limit 128 nowych tokenów, tryb non-thinking, temperatura 0 i seed 42. W podstawowym porównaniu 4B przez llama_cpp okno miało 512 tokenów, a qairt korzystał z konfiguracji paczki. Osobne serie długości wejścia mają stałe okno 16 384 tokenów.

Każda próba jest pierwszym żądaniem w nowym procesie. Próba rozgrzewkowa może rozgrzać system i cache plików, ale nie instancję runtime’u kolejnej próby. Raportowane czasy generacji nie obejmują ładowania modelu. Ten sam tekst promptu ma 48 tokenów wejścia w llama_cpp i 61 w qairt. Obie ścieżki używają różnych artefaktów i konfiguracji, więc porównanie TTFT nie izoluje wpływu sprzętu.

Stany zasilania ustawiałem ręcznie: zasilacz, bateria oraz bateria z Power Saving.

4. Wyniki: szybkość, energia i długość wejścia

Wykresy pokazują wyniki zapisanych prób. Kreski na wykresach szybkości oznaczają minimum i maksimum, nie przedział ufności. AC oznacza zasilacz, a Power Saving to tryb oszczędzania energii.

4.1 Szybkość: 4B i trzy stany zasilania

Mediana z 5 prób po 1 próbie wprowadzającej. Wyjątek to CPU z Power Saving, gdzie próba wprowadzająca przekroczyła limit czasu. Limit odpowiedzi to 128 tokenów, bez trybu thinking. Dla llama_cpp okno ma 512 tokenów, a dla qairt wynika z paczki.

Tempo decode w trzech stanach zasilania, z zakresem min–max
Ścieżka decode AC (tok/s) decode bateria (tok/s) decode bateria + Power Saving (tok/s)
llama_cpp CPU 33,0 33,1 ⏱ timeout
llama_cpp GPU (Adreno) 22,9 21,8 13,6
GenieX qairt (QNN → NPU) 21,3 21,4 20,4
llama_cpp hybrid 19,6 18,8 11,3
llama_cpp NPU (ggml-hexagon) 16,8 16,9 16,7
TTFT w trzech stanach zasilania, z zakresem min–max

qairt miał najkrótszy TTFT: mediany 89–98 ms zależnie od stanu zasilania. CPU osiągnął 150–158 ms, NPU przez ggml-hexagon 158–187 ms, hybrid 207–220 ms, a GPU 278–291 ms. To wyniki całych ścieżek, nie samych urządzeń: qairt przetwarzał 61 tokenów wejścia, a llama_cpp 48.

4.2 Średnia moc: ile watów pokazał pomiar?

Żeby porównanie było czytelniejsze, pokazuję średnią moc netto w watach (W). Waty mówią, jak szybko zużywana jest energia, a nie ile zużyto jej na całą odpowiedź. Niższa moc nie zawsze oznacza mniejszą energię zadania, jeśli model musi pracować znacznie dłużej.

To przybliżenia kanału SYS po odjęciu osobno zmierzonego spoczynku, nie całkowity pobór laptopa z gniazdka ani pomiar samego NPU. Średnią obliczyłem z odczytanej energii i czasu uruchomienia, razem z ładowaniem modelu. Historyczne odczyty obu serii wymagały różnego przeliczenia jednostek. Dokładny zakres licznika i błąd pomiaru nie są ustalone.

Przybliżona średnia moc netto SYS
Konfiguracja Średnia moc netto SYS, W ≈ Czas uruchomienia, s Tokeny odpowiedzi
qairt, zasilacz 9,0 96 1563
CPU, zasilacz 44,0 62 1448
GPU, zasilacz 17,9 96 2048
qairt, bateria + Power Saving 4,1 117 1563
GPU, bateria + Power Saving 7,9 201 2048
CPU, bateria + Power Saving, timeout 9,4 1802 Brak ukończonego wyniku

Najprościej: w osobnej próbie na zasilaczu odczyt wynosił około 44 W dla CPU, 18 W dla GPU i 9 W dla qairt na NPU. Na baterii z Power Saving qairt pokazał około 4 W, a GPU około 8 W. To obiecujący kierunek dla NPU, nie prognoza czasu pracy baterii.

Każda konfiguracja ma tylko jedną próbę, a odpowiedzi różnią się długością i treścią. Dlatego nie traktuję tych liczb jako porównania kosztu jednakowego zadania. Wynik CPU z timeoutem jest osobną obserwacją, nie ukończonym testem. Seria baterii zmienia jednocześnie źródło zasilania i tryb pracy, więc nie izoluje wpływu samego Power Saving.

4.3 Wnioski: co wybrać na baterii i z Power Saving?

Samo odłączenie zasilacza nie zmieniło mocno szybkości w tym krótkim teście: decode różnił się o mniej niż 1% dla CPU i obu ścieżek NPU, o około 4,6% dla GPU i 4,1% dla hybrid.

W tym teście z Power Saving wybrałbym NPU ze względu na szybkość generowania. qairt utrzymał około 20,4 tok/s, a ggml-hexagon 16,7 tok/s. GPU spadło do 13,6 tok/s, hybrid do 11,3 tok/s, a CPU nie ukończył próby wprowadzającej w limicie 15 minut. Podczas tej próby obserwowałem pracę 8 z 12 wątków, ale nie ustaliłem przyczyny opóźnienia.

NPU miało więc najwyższe tempo decode spośród badanych konfiguracji w tym trybie, choć nie było jedynym działającym wyborem: GPU nadal generowało tekst. Niższy przybliżony odczyt mocy qairt w osobnej, pojedynczej próbie jest obiecującą obserwacją, ale nie dowodem mniejszej energii jednakowego zadania ani dłuższej pracy na baterii. Rekomendację opieram przede wszystkim na utrzymanej szybkości w Power Saving.

4.4 Kwantyzacja: 4B na CPU i NPU przez ggml-hexagon

Zasilacz, krótki prompt, mediana z 5 prób. Na wykresie decode, czyli tempo generowania kolejnych tokenów.

Wpływ kwantyzacji GGUF na decode CPU i NPU
Kwantyzacja CPU NPU (ggml-hexagon)
F16 10,3 7,0
Q8_0 19,2 12,1
Q4_K_M 18,5 12,3
Q4_0 33,0 16,8

Osobna paczka qairt QNN W4A16 osiągnęła 21,3 tok/s. Nie umieszczam jej w tej samej serii kwantyzacji GGUF.

Wpływ kwantyzacji GGUF na prefill CPU i NPU

Q4_0 był na CPU około 3,2 razy szybszy niż F16 w decode i ponad 12 razy w prefillu. NPU miało wolniejszy decode dla wszystkich tych GGUF, ale mniejsze różnice prefillu między kwantyzacjami. To wynik tego modelu i buildu, nie ogólna hierarchia schematów. W tej serii mierzyłem szybkość, nie jakość odpowiedzi.

4.5 Liczba parametrów: od 1.7B do 32B na CPU

Zasilacz, Q4_K_M, non-thinking, po 5 mierzonych prób.

Tempo decode i TTFT modeli Qwen3 od 1.7B do 32B na CPU
Liczba parametrów decode (tok/s) prefill (tok/s) TTFT (ms)
1.7B 57,8 500,4 104
8B 18,4 98,3 530
14B 10,5 56,5 921
32B 4,8 22,3 2335
  • To oryginalny Qwen3 z przełączanym thinking/non-thinking, tutaj z wyłączonym thinking. 4B-2507 to inny checkpoint, więc nie wchodzi wprost w tę serię. Co z tego jest praktyczne?

  • 1.7B: największy zapas szybkości. Warto zacząć od niego przy prostych, wąskich zadaniach, ale sprawdzić, czy jakość wystarcza.

  • 8B: około 18 tok/s przy krótkim wejściu to rozsądny punkt startowy do interaktywnej pracy.

  • 14B: około 10,5 tok/s, czyli wolniejsza, ale nadal sensowna opcja, jeśli większy model daje lepszy wynik w konkretnym zadaniu.

  • 32B: około 4,8 tok/s. Sam decode 128 tokenów zajmuje orientacyjnie 27 s. W tym teście było to dla mnie zbyt wolne do wygodnego, interaktywnego czatu. Nie wyklucza to zastosowań wsadowych, jeśli dłuższe oczekiwanie jest akceptowalne, a jakość odpowiedzi uzasadnia wybór większego modelu; tego tutaj nie oceniałem.

32B jest największym modelem, dla którego uzyskałem wynik w tej serii, nie ustalonym limitem tego laptopa. Te próby mają krótkie wejście i okno 512 tokenów. Szybkość nie dowodzi jakości, a dłuższy dokument może znacznie zmienić responsywność.

4.6 Pojemność okna i rzeczywista długość wejścia

Zmiana pojemności okna przy krótkim prompcie (CPU, AC)

Zmieniałem pojemność okna, zostawiając 52 tokeny wejścia i 128 tokenów odpowiedzi. Historia rosła więc do około 180 pozycji, nie do 65k czy 262k. Poniżej mediany TTFT z trzech prób na komórkę.

Mapa median TTFT przy zwiększaniu pojemności okna i stałym krótkim wejściu
Liczba parametrów 512 16 384 65 536 131 072 262 144
8B 0,41 s 0,49 s 0,87 s 0,46 s 6,95 s
14B 1,01 s 1,07 s 1,70 s 11,46 s 9,61 s
32B 2,33 s 3,35 s 11,41 s 24,91 s 26,16 s

Przy większych limitach TTFT wzrastał, ale nie monotonicznie. Nie ustaliłem przyczyny ani praktycznego limitu długości dokumentu: ten test nie zapełniał okna.

Dłuższe wejście przy stałym oknie (4B Q4_0, CPU i NPU)

Na zasilaczu, z profilem maksymalnej wydajności, zostawiłem okno 16 384 tokenów i zwiększałem długość syntetycznego wejścia z powtarzanych akapitów. Najpierw wykonałem serię CPU, potem NPU przez GenieX llama_cpp, czyli ggml-hexagon, nie qairt. Użyłem tego samego zarejestrowanego Qwen3-4B-Instruct-2507 Q4_0, identycznych promptów, temperatury 0 i trzech mierzonych prób na komórkę, po jednej próbie wprowadzającej. Każda próba to nowy proces, a każda odpowiedź miała dokładnie 128 raportowanych tokenów i kończyła się limitem długości.

Wpływ długości wejścia na TTFT, decode i czas generacji CPU oraz NPU
Tokeny wejścia TTFT CPU TTFT NPU Decode CPU, tok/s Decode NPU, tok/s Cała generacja CPU Cała generacja NPU
420 1,99 s 0,66 s 27,22 14,79 6,69 s 9,31 s
1572 8,63 s 2,72 s 22,25 12,44 14,38 s 13,02 s
6180 65,12 s 16,42 s 6,18 7,74 86,03 s 32,96 s

Cała generacja to mediana całkowitego czasu generacji raportowanego przez silnik, bez ładowania modelu, nie suma median innych metryk. Prefill CPU wyniósł odpowiednio 211,2, 182,1 i 94,9 tok/s, a NPU 640,9, 577,1 i 376,4 tok/s.

Przy dłuższym wejściu NPU może być szybsze od CPU. Dla 6180 tokenów wejścia i 128 tokenów odpowiedzi generacja trwała około 33 s na NPU wobec 86 s na CPU. Przy najkrótszym wejściu wcześniej kończył CPU. Wybór zależy więc również od długości wejścia i odpowiedzi.

To dwa osobne wyniki: przewagę szybkości dłuższego wejścia zmierzyłem przez ggml-hexagon, a niższą moc w innej serii przez qairt. Nie zmierzyłem mocy ani energii tego testu dłuższego wejścia.

Pomiędzy krańcami decode CPU spadł o około 77%, a NPU o 48%. Nie przypisuję spadku jednemu mechanizmowi: wejście jest syntetyczne, serie szły od najkrótszej, bez rejestracji temperatury i taktowania. Nie zmierzyłem tu energii ani jakości pracy z dokumentami.

5. Co wybrać do konkretnego zastosowania

5.1 Praktyczne wnioski na podstawie tych prób

Nie wybierałbym jednej konfiguracji do wszystkiego. Najpierw ustaliłbym, czy ważniejsze są szybkość odpowiedzi, praca na baterii, długość dokumentu czy możliwości większego modelu. Poniższe propozycje są punktem startowym do własnych zadań, nie rankingiem jakości modeli ani gwarancją zachowania każdej aplikacji.

Potrzebujesz Punkt wyjścia Co potwierdzają próby
Szybkie generowanie przy krótkim wejściu na zasilaczu 4B Q4_0 przez GenieX llama_cpp na CPU Około 33 tok/s. W osobnej próbie CPU miał wyższą średnią moc netto niż qairt
Responsywne generowanie na baterii z Power Saving NPU: GenieX qairt, alternatywnie llama_cpp Odpowiednio 20,4 i 16,7 tok/s. Około 4 W netto SYS dla qairt to osobna, pojedyncza obserwacja, nie pomiar czasu pracy na baterii
Większy model do interaktywnej pracy Zacznij od 8B lub 14B Q4_K_M na CPU Około 18,4 i 10,5 tok/s przy krótkim wejściu. Jakość trzeba ocenić na własnym zadaniu
Dłuższe wejście i krótka odpowiedź llama_cpp na NPU Przy 6180 tokenach wejścia i 128 wyjścia około 33 s generacji wobec 86 s na CPU

5.2 Moje podsumowanie

NPU okazało się użyteczne i będę sprawdzał je dalej. Nie jest jednak automatycznie najlepsze tylko dlatego, że zostało zaprojektowane do obliczeń sieci neuronowych:

  • Krótkie wejście i zasilacz: CPU z Q4_0 dawało najwyższe tempo decode badanego 4B. Do szybkiej odpowiedzi nie muszę na siłę używać NPU.
  • Power Saving: NPU zachowało responsywność lepiej niż GPU i hybrid. Dlatego qairt jest dla mnie pierwszym kandydatem do generowania w tym trybie. Niższy przybliżony odczyt SYS w osobnej próbie jest dodatkową obserwacją, nie potwierdzeniem dłuższej pracy na baterii.
  • Dłuższe wejście: ggml-hexagon potrafił wygrać z CPU czasem całej odpowiedzi. Benchmark krótkiego pytania nie wystarcza do wyboru konfiguracji dla dokumentów.
  • Wielkość modelu: 8B i 14B wyglądają rozsądnie pod względem szybkości. 32B (około 4,8 tok/s) było dla mnie zbyt wolne do wygodnego czatu, ale może być przydatne w zadaniach wsadowych. Więcej parametrów ma sens tylko wtedy, gdy poprawia wynik konkretnego zadania; jakość trzeba sprawdzić osobno.

Następny krok to sprawdzić, czy wybrane modele rzeczywiście radzą sobie z realnymi zadaniami: poprawianiem tekstu, streszczaniem notatek, wydobywaniem informacji z dokumentów albo pomocą przy kodzie.

Chcę też sprawdzić, jak przełożyć działające konfiguracje na codzienne użycie, np. w LM Studio, Ollamie lub AnythingLLM.

LLM nie wyczerpuje też możliwych zastosowań NPU. Transkrypcja nagrań czy przetwarzanie obrazu to kolejne kierunki do sprawdzenia, z innymi modelami i wymaganiami.