LLM w kieszeni: od Termuxa do NPU w telefonie
Czy telefon może samodzielnie uruchamiać model językowy i wykonywać użyteczne zadania, bez internetu i bez wysyłania danych do chmury? Przez ostatnie miesiące sprawdzałem to na trzech urządzeniach: ROG Phone 9, starszym ROG Phone II i Samsungu Galaxy S21.
Zaczynałem od uruchamiania modeli przez Termuxa. Później połączyłem telefony w mały klaster, próbowałem sterować Androidem przez agenta AI, a ostatecznie zbudowałem aplikację odpowiadającą na wiadomości z Messengera.
Największym odkryciem nie była jednak sama możliwość uruchomienia LLM na telefonie. Była nim różnica między liczeniem na CPU a wykorzystaniem wbudowanego akceleratora NPU.
Pierwsze próby: Termux i llama.cpp
Zacząłem od llama.cpp uruchomionego bezpośrednio w Termuxie. Na ROG Phone 9 model Qwen3.5-4B w kwantyzacji Q4 generował około 10 tokenów na sekundę. Do krótkich odpowiedzi wystarczało, ale bardziej rozbudowane zadania szybko ujawniały ograniczenia.
Pierwszym problemem była pamięć. Telefon ma 11 GB RAM, lecz Android i działające aplikacje zostawiają zwykle tylko około 3,4 GB wolnego miejsca. Większe modele, choć fizycznie mieściły się na karcie pamięci, nie dawały się nawet załadować.
Samo miejsce przechowywania modelu też miało znaczenie. Odczyt przez warstwę FUSE z karty SD wydłużał ładowanie i nieco spowalniał generowanie. Uruchamianie modelu z katalogu domowego Termuxa działało sprawniej.
Próbowałem również wykorzystać GPU Adreno przez OpenCL i Vulkan. Bez roota nie udało mi się jednak pokonać ograniczeń dostępu do systemowych sterowników Androida. Późniejsza próba z bibliotekami Snapdragon także zakończyła się błędem OpenCL.
Osobną lekcją były modele z trybem myślenia. Przygotowałem niewielki zestaw zadań z wiedzy, matematyki, logiki i programowania. Okazało się, że ograniczenie budżetu myślenia do 2048 tokenów potrafi być gorsze od całkowitego wyłączenia tej funkcji. Model zużywał cały limit na wewnętrzne rozumowanie i nie zostawiał miejsca na odpowiedź.
W tym teście dla modeli 4B i 9B najlepszy stosunek jakości do czasu dawało wyłączenie myślenia. Trzeba było tylko dopilnować, aby llama.cpp rzeczywiście przekazywał odpowiednie ustawienia do szablonu rozmowy.
Trzy telefony, jeden klaster
W czerwcu połączyłem wszystkie trzy telefony przez hotspot Wi-Fi i uruchomiłem demonstrator działający całkowicie offline.
ROG Phone 9 pełnił funkcję głównego węzła z modelem Qwen3.5-4B. Dwa pozostałe telefony pracowały jako pomocnicy, wykorzystując mniejsze modele przez Ollamę. Orkiestrator napisałem w Node.js, bez dodatkowych zależności.
System potrafił równolegle porównywać odpowiedzi modeli, prowadzić dyskusję między agentami o różnych rolach, generować kod i sterować telefonem językiem naturalnym.
Najciekawszy wniosek dotyczył współpracy modeli. W trybie programowania cały użyteczny kod generował w praktyce główny telefon. Słabsze urządzenia odpowiadały zbyt wolno, żeby ich udział miał sens.
Był to jednodniowy pokaz, bez formalnych kryteriów oceny.
Później sprawdziłem również sterowanie Androidem przez adb. Agent miał wykonywać proste polecenia, takie jak włączenie Bluetooth czy zmiana trybu wyświetlania.
Warstwa bezpieczeństwa zadziałała: blokowała dostęp do wrażliwych aplikacji i niebezpiecznych przycisków. Gorzej było ze skutecznością. Małe modele gubiły się w interfejsie, powtarzały błędne działania albo zgłaszały sukces bez wykonania zadania.
Co więcej, weryfikacja ujawniła błędy w moich własnych testach. Jeden ze skryptów uznawał wynik kalkulatora za poprawny, ponieważ znajdował szukane cyfry we współrzędnych elementów ekranu, a nie w samym wyniku.
Porzuciłem więc sterowanie przez adb i przeszedłem do dedykowanej aplikacji.
Przełom: Hexagon NPU
Zbudowałem natywną aplikację na Androida, która odbiera powiadomienia z Messengera, przekazuje ich treść do lokalnego modelu i przygotowuje odpowiedzi.
Wiadomości można zatwierdzać ręcznie albo wysyłać automatycznie. W tym drugim trybie dodałem dzienne limity na rozmówcę i symulację tempa pisania. Logika aplikacji przeszła wszystkie 141 testów jednostkowych.
Najważniejszą zmianą było uruchomienie llama.cpp na akceleratorze Hexagon NPU zamiast CPU.
Nie zadziałało od razu. Pierwsza wersja po cichu wracała do CPU z powodu brakującej biblioteki OpenCL. Pomogło jawne wskazanie ścieżek bibliotek Snapdragon. Musiałem również zmniejszyć ustawienia pamięci podręcznej serwera.
Różnica była ogromna. Na ROG Phone 9 NPU przetwarzało prompt z prędkością 715 tokenów na sekundę, a odpowiedzi generowało w tempie kilkunastu do ponad dwudziestu tokenów na sekundę.
W zmierzonym przypadku serwer zajmował zaledwie 0,67 GB pamięci PSS. Dla porównania, wcześniejsze uruchomienie na CPU zajmowało 5,3 GB RAM i przy zapchanej pamięci zwalniało poniżej jednego tokenu na sekundę.
To właśnie przeniesienie obliczeń na NPU zmieniło eksperyment w coś praktycznie użytecznego.
Mały model potrzebuje pomocy kodu
Pozostał wybór modelu do naturalnych, krótkich rozmów. Przetestowałem kilka modeli, oceniając ich odpowiedzi na przygotowanym zestawie konwersacji.
Wybrałem Gemma 4 E2B. Wypadł najlepiej pod względem przyjętych kryteriów, choć przewaga nie była jednoznaczna. Jeden sędzia LLM wskazał go jako zwycięzcę, drugi wskazał remis.
Ciekawszy problem pojawił się przy kończeniu rozmów. Model nie zawsze rozpoznawał, że powinien przestać odpowiadać. Potrafił kontynuować wymianę pożegnań albo wysyłać rozmówcy techniczny znacznik [OK].
Zastąpiłem tę decyzję regułą w kodzie. Na odłożonym zbiorze poprawnie rozpoznawała niemal wszystkie zakończone rozmowy i zatrzymywała większość pętli. Miała jednak koszt: czasami milczała, choć odpowiedź była potrzebna.
Sprawdziłem też automatyczne dopasowywanie stylu do rozmówcy. Metoda działała na danych syntetycznych, ale przesadzała z charakterystycznymi zwrotami. Jeśli ktoś czasami pisał „xD”, model potrafił dodawać je do każdej odpowiedzi.
To kolejny przykład, że małemu modelowi nie warto oddawać wszystkich decyzji.
Android pozostaje największą przeszkodą
Sama inferencja to tylko część problemu. Android potrafi zabijać procesy po zablokowaniu ekranu, ograniczać pracę w tle i zamykać aplikacje przy niedoborze pamięci.
W Termuxie musiałem walczyć z niestabilnymi repozytoriami, procesami sshd, problemami z proot i ograniczeniami systemowego linkera. W aplikacji dodałem mechanizmy ochrony przed trybem Doze i ponownego uruchamiania serwera po zmianie konfiguracji.
Nie wszystko zostało sprawdzone. Nadal nie mam dokładnego pomiaru zużycia pamięci przez wybrany model w działającej aplikacji, długoterminowego wpływu NPU na baterię i temperaturę ani rzeczywistego czasu budowania profilu rozmówcy z dużej historii wiadomości bezpośrednio na telefonie.
Z tych eksperymentów wyniosłem dwa wnioski. Po pierwsze, na tym samym telefonie przejście z CPU na NPU najbardziej przyspieszyło przetwarzanie promptu i zmniejszyło zużycie pamięci. Po drugie, szybko działający model nie oznacza jeszcze niezawodnego agenta.
Najlepiej sprawdza się podział obowiązków: model generuje język, a zwykły kod pilnuje ograniczeń, bezpieczeństwa i decyzji, które można podjąć deterministycznie.
Telefon potrafi już całkiem sprawnie uruchamiać lokalne AI. Trudniejsze okazuje się doprowadzenie do tego, żeby cały system działał przewidywalnie, również wtedy, gdy ekran jest zgaszony, pamięć się kończy, a model po prostu nie wie, kiedy zamilknąć.