Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: Grzegorz 'baziorek' Bazior
Inne artykuły

[C++, llama.cpp] używanie sztucznej inteligencji z poziomu C++

[artykuł]
W obecnych czasach trudno sobie wyobrazić, że jeszcze kilka lat temu sztuczna inteligencja nie była tak popularna dla zwykłego użytkownika. Wcześniej kojarzyła się głównie z popkulturą: filmami typu Matrix czy Terminator oraz książkami science-fiction.

Sztuczna inteligencja istnieje od bardzo dawna, jednak dopiero udostępnienie przez OpenAI modelu LLM (Large Language Model) dla wszystkich o nazwie ChatGPT nakręciło lawinę. Dzięki temu wiele osób zaangażowało się w sztuczną inteligencję, a część osób wypuszczało własne otwarte modele, które można używać na własnym komputerze bez kosztów za użycie API. Obecnie jest niezliczona liczba modeli AI do różnych celów, w różnych wariantach, które możemy użyć.
Właśnie dzięki tej popularności AI język Python stał się ostatnimi latami najpopularniejszym językiem programowania (według TIOBE Index). Jeszcze w 2018 roku C++ był od niego popularniejszy, a Java zajmowała pierwsze miejsce.

Co istotne – zajmując się sztuczną inteligencją nie jesteśmy skazani na Pythona. Możemy bez problemu używać innych języków, w tym właśnie C++.

Dlaczego C++ „pod spodem”?

Większość wydajnych silników do lokalnej inferencji LLM jest napisana właśnie w C/C++. Python to najczęściej tylko wygodna warstwa użytkowa (bindings).

Popularne silniki do sztucznej inteligencji

Najważniejsze przykłady silników do używania modeli sztucznej inteligencji:
Silnik Charakterystyka Format plików
llama.cpp Czyste C/C++, zero ciężkich zależności. Zbudowany na bibliotece ggml – to autorska, niskopoziomowa biblioteka tego samego autora do szybkich operacji na tensorach (coś jak mały, wydzielony rdzeń PyTorcha napisany w C), obsługująca zarówno CPU jak i różne GPU oraz kwantyzację. To właśnie ggml odpowiada pod spodem za szybkość i małe zależności llama.cpp. GGUF
ONNX Runtime Napisany w C++. Dobry wybór przy formacie ONNX i szerokim wsparciu urządzeń (w tym NPU). ONNX Runtime GenAI to nie osobny, konkurencyjny silnik – to biblioteka-nakładka na ten sam ONNX Runtime, która dorzuca gotową pętlę generowania tekstu (tokenizację, zarządzanie KV cache, próbkowanie), żeby nie pisać tego ręcznie. ONNX
TensorRT-LLM (Nvidia) Rdzeń w C++ (Python służy głównie do definicji modelu i budowania silnika). Bardziej złożony we wdrożeniu, za to mocno zoptymalizowany pod karty Nvidia. .engine (skompilowany silnik) z pośredniego checkpointu .safetensors
OpenVINO (Intel) Rdzeń w C++. Szerokie wsparcie urządzeń (CPU, iGPU, dGPU, NPU) – niekoniecznie od Intela. .xml (topologia) + .bin (wagi) – tzw. OpenVINO IR
LibTorch (C++ API PyTorcha) Maksymalna zgodność z modelami wprost z PyTorcha, ale większe zależności i cięższy runtime. TorchScript / .pt
Poza wymienionymi silnikami istnieje jeszcze kilka innych, mniej popularnych backendów GPU pod konkretny sprzęt (np. rozwiązania AMD) – celowo pomijamy je tutaj, żeby w tabeli zostały tylko najważniejsze pozycje.
Zamiast któregoś z powyższych silników pewnie kojarzysz też Ollamę – to jednak nie jest osobny silnik konkurencyjny wobec llama.cpp, tylko napisana w Go nakładka, która pod spodem sama korzysta z llama.cpp/ggml (dorzucając wygodne zarządzanie modelami i prosty interfejs). Świetna jako gotowe narzędzie end-user, ale nie ma czego linkować do własnego programu w C++ – dlatego nie ma jej w powyższej tabeli.
Uwaga na nazewnictwo:Llama" (z wielkiej litery) to rodzina modeli językowych firmy Meta. „llama.cpp" (z małej litery, z rozszerzeniem w nazwie) to niezależny, społecznościowy silnik do uruchamiania modeli – nazwany tak, bo pierwotnie służył właśnie do odpalania modeli Llama. Dziś jednak llama.cpp uruchamia praktycznie dowolną architekturę modelu open source (Qwen, Gemma, Mistral, Bielik i wiele innych), nie tylko modele od Mety. Te dwie rzeczy nie mają ze sobą nic wspólnego organizacyjnie – Meta nie ma nic wspólnego z rozwojem llama.cpp.
Jak widać używanie modeli bezpośrednio z poziomu C++ jest naturalne i często szybsze oraz lżejsze niż przez Pythona, a także wymaga mniej zależności. Dla większości osób zaczynających przygodę z AI w C++ najlepszym wyborem jest llama.cpp, dlatego więc ten artykuł będzie się na tym skupiał.

O czym jest ten artykuł

Celem tego artykułu jest pokazanie, że w C++ nie jest wcale trudno używać sztucznej inteligencji. Modele językowe w każdym języku wymagają tych samych kroków: wyboru biblioteki, pobrania odpowiedniego modelu i jego uruchomienia, interakcji z załadowanym modelem.
Artykuł jest swego rodzaju wstępem, jak zacząć używanie modeli otwarto źródłowych nie mając na ten temat jeszcze doświadczenia. Są tam informacje o pobieraniu modeli, o pewnych zagadnieniach związanych z modelami (chociażby jak czytać ich nazwy), GPU vs CPU itd. Dla osób biegłych w sztucznej inteligencji to są podstawy, więc jeśli chcecie zagłębić używanie AI z poziomu C++ to chyba najszybciej będzie dla Was zagłębić się w oficjalne przykłady llama.cpp.
Ten artykuł nie jest o tym, jak stworzyć czy trenować własny model. Skupiamy się wyłącznie na używaniu istniejących modeli open source w aplikacji napisanej w C++.
Zagadnienie sztucznej inteligencji jest bardzo szerokie – jeśli chcesz być w nim naprawdę dobry, wymaga lat nauki i praktyki.
Jeśli szukasz szerszego wprowadzenia do sztucznej inteligencji i klasycznego uczenia maszynowego w C++ aby zrozumieć szerszy kontekst AI w C++ możesz sięgnąć po AI With Modern C++ (Ayman Alheraki, II edycja) (nie jest to jednak poradnik o budowaniu własnego LLM-a od zera).

Modele to nie encyklopedia!!!

Ważne: Modele językowe to nie encyklopedia ani źródło prawdy. Pod spodem to matematyka i prawdopodobieństwo – model przewiduje najbardziej prawdopodobne kolejne tokeny na podstawie danych, na których był trenowany. Dlatego pojawiają się halucynacje (pewne siebie, ale fałszywe informacje).
Dodatkowo modele są zwykle dodatkowo trenowane (alignment / RLHF / safety training), żeby spełniać wymogi prawne i „poprawność polityczną”. Skutkuje to czasem podwójnymi standardami – klasyczny przykład: ChatGPT często odmawia dumy z bycia białym, a jednocześnie zachęca do dumy z bycia czarnoskórym (przykład z X). Warto o tym pamiętać przy krytycznych zastosowaniach.
Jak chcesz z kolei lepiej zrozumieć działanie modeli jak chatGPT polecam artykuły Jak działa ChatGPT - Trzy Kody lub Jak działa ChatGPT… | Wojciech Myszka.
Prawdy trzeba poszukiwać, nie bazować wyłącznie na tym co podane na tacy!

Wybór modelu

Istnieje niezliczona liczba modeli do uruchomienia u siebie na komputerze, skupimy się na modelach otwartych (niewymagających płatnego API), które każdy może pobrać i uruchomić. Nie są to modele do wszystkiego, dlatego w Twoim zastosowaniu musisz dobrać model pasujący pod Twoje zastosowanie. Modele mają różne wersje, ale o tym później.
Na sam początek (szczególnie gdy nie masz mocnego GPU pod AI) warto zacząć od małych modeli:
  • Qwen3 0.6B / 1.7B / 4B – świetne do interakcji w stylu „hello world”. Szybkie nawet na samym CPU. Gotowe GGUF na Hugging Face Qwen (oryginalne wagi) oraz przekonwertowane, gotowe do użycia GGUF-y pod ggml-org.
  • Ling-3.0-tiny – nazwa trochę myli: to model typu MoE (Mixture of Experts) z 7,9 mld parametrów całkowitych, z czego przy każdym tokenie aktywowane jest tylko ok. 1,3 mld. Dzięki temu liczy się bardzo szybko (koszt obliczeniowy bliski modelowi 1,3B), ale do samego wczytania do pamięci wciąż potrzeba miejsca na wszystkie 7,9 mld wag (Q4 to ok. 4–5 GB) – to nie jest model „na słabym laptopie bez GPU” w sensie rozmiaru, tylko w sensie szybkości liczenia. (inclusionAI/Ling-3.0-tiny-GGUF na Hugging Face)
  • Qwen3-8B – kolejny stopień w tej samej rodzinie co model 0.6B/1.7B/4B wyżej, tylko z większą liczbą parametrów: zauważalnie lepsza jakość odpowiedzi i rozumowania, kosztem wyraźnie wolniejszego działania (szczególnie na samym CPU) oraz większego zapotrzebowania na pamięć (w Q4_K_M to ok. 5–6 GB – więcej o tym w sekcji o wymaganiach VRAM/RAM niżej). Dobry wybór, gdy mały model 0.6B/1.7B już nie wystarcza jakościowo, a nie masz jeszcze GPU do modeli 30B+. Gotowe GGUF-y na tych samych profilach Qwen oraz ggml-org co model 0.6B/1.7B/4B.
  • Bielik – polska rodzina modeli (obecnie generacja 3.0/2.x: warianty ok. 1,5B / 4,5B / 11B), tworzona przez społeczność SpeakLeash wspólnie z Cyfronet AGH – czyli tą samą infrastrukturą superkomputerową, z której korzysta AGH (najszybszy superkomputer w Polsce (Helios) i 116. na świecie wg listy TOP500 z czerwca 2026). Bardzo dobrze radzi sobie z językiem polskim, słabiej wypada w kodowaniu niż modele wyspecjalizowane pod programowanie. Licencja Apache 2.0. Ma też swój chat pod adresem bielikchat.pl .
  • Gemma-3 / Gemma-4 (Google) – solidna jakość, dostępne też wersje z wizją (czyli takie, które oprócz tekstu przyjmują jako wejście również obraz – np. potrafią opisać zdjęcie albo przeczytać z niego tekst).
  • PLLuM (Polish Large Language Model, nie „Plumm") – kolejna polska rodzina modeli, rozwijana przez konsorcjum NASK i partnerów (m.in. ACK Cyfronet AGH) w ramach projektu HIVE AI. To właśnie PLLuM stoi za asystentem AI w aplikacji mObywatel oraz za chatbotami urzędowymi w kilku urzędach. Dostępny w kilku wariantach, oficjalne wagi są pod organizacją CYFRAGOVPL. Mocna strona to polszczyzna i pisma urzędowe, słabsza – kodowanie.
  • Qwen2.5-Coder-7B-Instruct – wariant rodziny Qwen wyspecjalizowany pod kod (w Ollamie bywa widoczny pod skróconą nazwą qwen2.5-coder), sensowny wybór, gdy zależy Ci akurat na wsparciu przy programowaniu (w tym w C++), a nie na ogólnej rozmowie. Gotowe GGUF-y wprost od Qwen oraz od niezależnych konwerterów (np. bartowski). Istnieje też dużo mocniejszy, ale i dużo cięższy Qwen3-Coder (m.in. wariant Coder-Next, model MoE) – ten wymaga już kilkudziesięciu GB pamięci, więc to już nie jest propozycja na sam start.
Powyższa lista to modele tekstowe (LLM) – tym zajmuje się llama.cpp i ten artykuł. Jeśli szukasz czegoś do generowania obrazów czy wideo, to inna kategoria modeli (dyfuzyjne, nie LLM) i inne narzędzie – warto zerknąć na stable-diffusion.cpp, siostrzany projekt zbudowany na tym samym ggml co llama.cpp, obsługujący m.in. Stable Diffusion, Flux czy modele wideo (Wan, LTX). Do generowania muzyki/mowy lokalnie, też na ggml, istnieje projekt audio.cpp.

Prywatność modeli uruchamianych lokalnie

Jak ktoś się boi o własną prywatność i przez to chce unikać modeli od Google, Facebook czy chińskich firm, to może być spokojny - sam plik z wagami modelu to statyczne liczby, które fizycznie nie mają jak "zadzwonić do domu". A sam silnik llama.cpp nie wykonuje żadnych połączeń sieciowych poza tymi, o które sam poprosisz (np. pobranie modelu z Hugging Face flagą
-hf
) - nie ma w nim telemetrii ani żadnego "phone-home". Właśnie po to m.in. sięgamy po modele lokalne.
Jedyne, o czym warto pamiętać: to dotyczy samego silnika llama.cpp uruchomionego offline. Jeśli wystawisz
llama-server
 na świat (nie tylko na
localhost
) albo użyjesz jakiejś nakładki/aplikacji budowanej na llama.cpp, to już ta konkretna nakładka (a nie sam model czy silnik) mogłaby teoretycznie gdzieś coś wysyłać - więc przy w pełni prywatnym setupie warto sprawdzić też kod tego, co dokładnie owija sam silnik.

Formaty modeli – GGUF, ONNX, wagi

Modele to przede wszystkim wagi (parametry) + konfiguracja + tokenizer.
Czym są wagi? Wagi (ang. weights) to ogromna liczba parametrów liczbowych – dla współczesnych modeli od setek milionów do bilionów – które model „nauczył się” podczas treningu. To po prostu wielkie tablice liczb zmiennoprzecinkowych, mnożone i sumowane z danymi wejściowymi przy generowaniu odpowiedzi – w nich zakodowana jest cała „wiedza” modelu. Plik modelu (np. .gguf) to w praktyce zrzut właśnie tych liczb na dysk, plus metadane (konfiguracja, tokenizer).
Najważniejsze formaty:
  • GGUF – format stworzony przez twórców llama.cpp. Najpopularniejszy do lokalnego uruchamiania. Wspiera kwantyzację (Q4_K_M, Q5_K_M, Q8_0 itd.).
  • ONNX – uniwersalny format, dobry do produkcji i różnych urządzeń.
  • Safetensors / PyTorch – oryginalne formaty z Hugging Face, które możemy przekonwertować do GGUF lub ONNX.
Kwantyzacja to klucz do uruchamiania dużych modeli na zwykłym PC. Polega na zmniejszeniu precyzji wag modelu (np. z 16-bitów do 4 lub 5 bitów). Dzięki temu model zajmuje znacznie mniej pamięci RAM/VRAM i działa szybciej, a spadek jakości odpowiedzi jest zwykle niewielki. Najczęściej stosowane kwantyzacje to Q4_K_M i Q5_K_M – dobry kompromis między rozmiarem a jakością.

Jak czytać nazwy modeli i co one oznaczają

Nazwy plików GGUF na pierwszy rzut oka wyglądają jak szyfr. Przykład:
Qwen3-1.7B-Instruct-Q4_K_M.gguf


Rozbijmy to na części:
  • Qwen3 – nazwa rodziny modelu
  • 1.7B – liczba parametrów (1,7 miliarda). Parametry to wagi (liczby) modelu – im więcej, tym model potrafi zapamiętać i „zrozumieć” więcej zależności, ale wymaga więcej pamięci i jest wolniejszy.
  • Instruct – model dostrojony do instrukcji/czatu (lepszy do rozmowy niż „base”)
  • Q4_K_M – kwantyzacja:
      - Q4 = ok. 4 bity na wagę
      - _K = nowsza, lepsza metoda K-quant
      -_M = Medium (kompromis jakość/rozmiar; jest też _S = Smaller i _L = Larger)

Warto też wiedzieć, że część nowszych modeli to MoE (Mixture of Experts) – w nazwie zobaczysz to jako np. „30B-A3B” (30 mld parametrów całkowitych, ale tylko ok. 3 mld aktywowanych na token). Takie modele liczą się szybko jak mały model, ale zajmują na dysku/w pamięci tyle, co model o pełnym (większym) rozmiarze.

Przybliżone wymagania VRAM/RAM dla modelu 8B (same wagi + mały kontekst):
  • Q4_K_M → ok. 5–6 GB
  • Q5_K_M → ok. 6–7 GB
  • Q8_0 → ok. 9–10 GB
  • FP16 (bez kwantyzacji) → ok. 16 GB
Do tego dochodzi KV cache (pamięć kontekstu) – im dłuższy kontekst, tym więcej VRAM.
Reguła kciuka: model X B w Q4_K_M zajmuje mniej więcej X/2 GB na dysku + trochę nadmiaru na kontekst.

Czas wykonania i zasoby

Szybkość zależy od wielu czynników: rozmiaru modelu, kwantyzacji, CPU vs GPU (karta graficzna), długości promptu i generowanej odpowiedzi.

Orientacyjne wartości (tokenów na sekundę):
  • Mały model 1–3B Q4 na dobrym CPU → 20–50+ t/s
  • Model 7–8B Q4_K_M na RTX 4060/4070 → 40–90 t/s
  • Model 7–8B tylko na CPU (nowoczesny) → 8–20 t/s
  • Model 70B Q4 na 24 GB VRAM (częściowy offload – część warstw modelu liczona na GPU, reszta na CPU, bo cały model się nie mieści w VRAM) → kilka–kilkanaście t/s
Im większy model i im niższa kwantyzacja (więcej bitów), tym wolniej i więcej pamięci. Dlatego często lepiej wziąć mniejszy, dobrze dostrojony model niż duży, który ledwo się mieści.

Czym jest token?

Skoro już kilka razy padło słowo „token”, warto rozjaśnić o co chodzi: token to najmniejszy kawałek tekstu, na jaki model dzieli wejście/wyjście – to niekoniecznie całe słowo. Może to być całe słowo, jego fragment, pojedynczy znak interpunkcyjny albo nawet kawałek słowa w innym języku. Model nie „czyta” liter ani słów – zamienia tekst na ciąg liczb (tokenów) za pomocą swojego tokenizera, przetwarza je, i tak samo token po tokenie generuje odpowiedź. Dlatego szybkość podaje się w tokenach na sekundę, a nie znakach czy słowach na sekundę, a rozmiar kontekstu (o którym więcej w dalszej części) też liczy się w tokenach, nie znakach.

Instalacja llama.cpp

Najbezpieczniej zainstalować llama.cpp ręcznie – mamy wtedy największą kontrolę nad optymalizacjami (CUDA, Vulkan, Metal).

Wspólna część dla kilku systemów:
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
Pinowanie wersji: llama.cpp nie taguje wydań jako tagów na githubie „v0.4.0" czy podobnie – używa numerów kolejnych buildów w stylu
bNNNNN
 (np.
b10944
). Projekt rozwija się bardzo szybko (potrafi być kilka–kilkanaście buildów dziennie), więc jeśli chcesz pinować konkretną, stabilną wersję zamiast śledzić
master
, sprawdź najnowszy tag na stronie wydań albo komendą:
git tag --sort=-creatordate | head -n 1

i dopiero ten konkretny tag zrób checkout, np.
git checkout b10944
. Sam numer wersji w pliku CMake (bywa widoczny jako coś w rodzaju „0.4.0-dev") to tylko wewnętrzny numerek projektu do celów pakietowania (np. vcpkg) – to nie jest tag, którego można użyć do
git checkout
.
Statyczny build i bez testów: do każdego z poniższych poleceń warto dodać
-DBUILD_SHARED_LIBS=OFF
 (link statyczny – gotowy program nie ciągnie za sobą plików .so/.dll, które trzeba by osobno kopiować/wskazywać) oraz
-DLLAMA_BUILD_TESTS=OFF
 (uwaga: to własna flaga llama.cpp, a nie standardowe BUILD_TESTING z CMake – to drugie nic by tu nie dało). Krótszy, czystszy build, bez wpływu na licencję (llama.cpp jest na MIT niezależnie od sposobu linkowania).

Linux

cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DLLAMA_BUILD_TESTS=OFF
cmake --build build -j$(nproc)
Z GPU (NVIDIA):
cmake -B build -DGGML_CUDA=ON -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DLLAMA_BUILD_TESTS=OFF
cmake --build build -j$(nproc)

Windows

cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DLLAMA_BUILD_TESTS=OFF
cmake --build build --config Release
Z GPU:
cmake -B build -DGGML_CUDA=ON -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DLLAMA_BUILD_TESTS=OFF
cmake --build build --config Release
Gotowe binarki:
winget install llama.cpp

(głównie CPU).

Po zbudowaniu: Linux i Windows

Po zbudowaniu w katalogu
build/bin
 pojawią się m.in.:
  • llama-cli
     – narzędzie linii poleceń do szybkiego testowania modeli
  • llama-server
    serwer HTTP (OpenAI-compatible API)
  • llama-mtmd-cli
    – wariant do modeli multimodalnych (obraz/audio/wideo) – więcej o nim w sekcji o przykładzie z obrazkiem, jednak obecnie
    llama-cli
     zawiera te funkcjonalności
  • biblioteka
    libllama
     – do linkowania we własnych programach C/C++.
Zmiana w nazewnictwie narzędzi (świeża sprawa): historycznie
llama-cli
 było jednym narzędziem łączącym tryb czatu i „surowego” dokańczania tekstu. W międzyczasie projekt to rozdzielił: stare narzędzie (z flagami, które można znaleźć w starszych tutorialach, np.
-cnv
) zostało przemianowane na
llama-completion
, a nazwa
llama-cli
 trafiła do nowego, przepisanego od zera narzędzia, które w tym artykule opisujemy. Jeśli więc trafisz na starszy poradnik i coś Ci się nie zgadza z opcjami – to najpewniej dlatego.
Warto wiedzieć: projekt ma też oficjalny instalator (llama.app) z jednym binarnym
llama
 i podkomendami – zamiast
llama-cli
 można tam użyć
llama cli
, analogicznie
llama serve
 zamiast
llama-server
. Dodatkowo od lipca 2026
llama-cli
 wewnętrznie odpala w tle
llama-server
 i łączy się z nim jak klient – bez znaczenia dla użytkownika, ale gdybyś się zastanawiał, skąd taka architektura.
W dalszej części artykułu skupimy się na
llama-cli
 aby pokazać szybkie użycie, następnie na
libllama
 aby pokazać jak coś podobnego zrobić w kodzie C++, z kolei
llama-server
 najwyżej jako deser. Idea serwera jest taka, że nie używamy biblioteki, tylko łączymy się z serwerem po REST API. Zaleta tego rozwiązania jest to, że ten serwer może faktycznie być na komputerze z dobrym GPU.

Inne sposoby integracji z projektem

  • vcpkg (rekomendowane na Windows):
    vcpkg install llama-cpp
     + w CMake'u
    find_package(llama REQUIRED)
     (więcej o użyciu managera pakietów vcpkg w oddzielnym artykule)
  • FetchContent (komenda w CMake'u, która pobiera z repozytorium zależności) – wygodne, ale wolne przy czystych buildach i trudniejsze w kontroli flag CUDA
  • Ręczna instalacja +
    find_package
     – najczystsze rozwiązanie, pokazujemy ją niżej przy pierwszym przykładzie w C++

Pierwsze uruchomienie – pobranie modelu i prosta komenda

Na start wybieramy mały model (np. Qwen3-1.7B), żeby działał sensownie nawet bez GPU.

Pobranie modelu:
# Najwygodniejszy sposób – bezpośrednio przez llama.cpp
llama-cli -hf ggml-org/Qwen3-1.7B-GGUF:Q4_K_M
Model zostanie pobrany do cache Hugging Face (zazwyczaj
~/.cache/huggingface
).
Uwaga na organizację w repozytorium: sam Qwen (twórca modelu) publikuje na Hugging Face oryginalne wagi, ale zwykle nie wagi w formacie GGUF. Gotowe GGUF-y dla modeli Qwen znajdziesz najczęściej pod organizacją
ggml-org
 (oficjalny użytkownik projektu llama.cpp) albo u niezależnych osób konwertujących modele, jak
bartowski
 czy
unsloth
. Jeśli komenda
-hf
 zgłasza, że nie znaleziono plików GGUF w repozytorium – sprawdź, czy nie próbujesz pobrać z organizacji, która publikuje tylko oryginalne (niekonwertowane) wagi.

Najprostsze uruchomienie:
llama-cli -m Qwen3-1.7B-Q4_K_M.gguf -p "Przetłumacz na polski: The weather is nice today."


Współczesny
llama-cli
 ma sensowne wartości domyślne, więc powyższa komenda wystarczy: sam wykryje, ile ma wątków CPU użyć, sam spróbuje dobrać podział CPU/GPU (patrz niżej), a kontekst domyślnie ustawi na maksimum, z jakim model był trenowany.
Jeśli zamiast tego jawnie wymusisz pracę tylko na CPU flagą
-ngl 0
 na buildzie skompilowanym bez wsparcia GPU, możesz zobaczyć komunikat:
warning: no usable GPU found, --gpu-layers option will be ignored
warning: one possible reason is that llama.cpp was compiled without GPU support
Komunikat ten pojawia się zawsze, gdy jawnie podasz flagę
-ngl
/
--gpu-layers
(niezależnie od wartości – nawet
0
!) na buildzie bez wsparcia GPU – bo sprawdzana jest tylko możliwość builda, a nie konkretna liczba. To nieszkodliwe ostrzeżenie: model i tak policzy się na CPU. Żeby go uniknąć, po prostu nie podawaj
-ngl
 wcale (skoro i tak nie masz GPU, wartość domyślna „auto" sama sobie z tym poradzi bez zbędnego ostrzeżenia). Gdy któregoś dnia zechcesz faktycznie używać GPU, przebuduj z flagą
-DGGML_CUDA=ON
 (lub odpowiednikiem dla Twojego backendu).

Przykład z dodatkowymi, jawnymi parametrami (przydatne, gdy chcesz mieć pełną kontrolę zamiast polegać na automatyce):
llama-cli -m Qwen3-1.7B-Q4_K_M.gguf \
  -ngl 0 \
  -t 6 \
  -c 2048 \
  -n 128 \
  --temp 0.3 \
  -p "Przetłumacz na polski: The weather is nice today."

Poniżej wyjaśnienie użytych argumentów:
  • -m model.gguf
     – ścieżka do pliku modelu (nie modelu)
  • -ngl auto
    (domyślnie) /
    -ngl 0
     /
    -ngl 99
     /
    -ngl all
     – liczba warstw modelu wrzucanych na GPU. To nie jest procent – to konkretna liczba warstw transformera. Czy np. 50 to „pół na pół” zależy od tego, ile warstw ma dany model w ogóle (widać to w logach startowych jako „offloading X/Y layers to GPU”) – dopiero znając Y możesz policzyć, jaki to ułamek.
    0
     = tylko CPU,
    all
     (lub duża liczba typu 999) = wszystko na GPU, wartość pośrednia = hybryda CPU+GPU. Domyślne
    auto
     każe llama.cpp samemu dobrać sensowny podział pod dostępną pamięć.
  • -t 8
    – liczba wątków CPU. Domyślnie tyle, ile fizycznych rdzeni wykryje system (nie trzeba tego ustawiać ręcznie, chyba że chcesz nadpisać automatyczną detekcję).
  • -c 4096
    – rozmiar kontekstu (w tokenach). Domyślnie
    0
    , co oznacza „weź maksymalny kontekst, z jakim model był trenowany” (ta informacja jest zapisana w metadanych pliku GGUF) – więc jeśli nic nie podasz, i tak dostaniesz sensowną wartość dopasowaną do konkretnego modelu.
  • -n 256
    – ile tokenów wygenerować (-1 = bez limitu)
  • -p "Twój prompt"
    – prompt startowy
  • --temp 0.7
    – temperatura (domyślnie 0.80; zwykle 0.0–1.5; wyższa = bardziej kreatywne/losowe odpowiedzi, niższa = bardziej przewidywalne)
  • --top-p 0.9
    – nucleus sampling, domyślnie 0.95 (bierze pod uwagę tylko te tokeny, których skumulowane prawdopodobieństwo nie przekracza podanej wartości; 1.0 wyłącza ten mechanizm)
  • -sys "instrukcja systemowa"
    – ustawia prompt systemowy; dzisiejszy
    llama-cli
     domyślnie formatuje rozmowę wg szablonu czatu modelu (patrz sekcja o szablonach czatu niżej), więc osobny „tryb konwersacji” nie jest już potrzebny
  • -f plik.txt
    – wczytuje prompt z pliku zamiast z
    -p
  • --list-devices
     – lista dostępnych urządzeń GPU + VRAM
  • -hf repo/model:quant
    – pobranie modelu prosto z Hugging Face (jeśli pominiesz kwantyzację, domyślnie próbuje Q4_K_M)

llama z poziomu kodu C++

llama.cpp oficjalnie udostępnia głównie C API. Nie ma oficjalnego, grubego wrappera C++ w głównym repozytorium. W razie potrzeby łatwo owinąć go we własne klasy C++.

Oficjalne przykłady z repozytorium llama.cpp warte obejrzenia:

Kompilacja własnego programu (CMake)

Żeby użyć
libllama
 we własnym, niezależnym projekcie, najpierw zbuduj i zainstaluj llama.cpp wg wcześniejszych instrukcji, następnie we własnym projekcie wystarczy taki
CMakeLists.txt
:
cmake_minimum_required(VERSION 3.12)
project(moj-przyklad)

find_package(Llama REQUIRED)
find_package(Threads REQUIRED)

add_executable(${PROJECT_NAME} main.cpp)
target_link_libraries(${PROJECT_NAME} PRIVATE llama ggml::all Threads::Threads)
target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_23)
i skonfigurowanie go tak, żeby
find_package
 znalazł zainstalowane wcześniej llama.cpp:
cmake -S . -B build -DCMAKE_PREFIX_PATH=/ścieżka/do/llama.cpp/inst/lib/cmake
cmake --build build
Gdy używasz akceleracji sprzętowej (CUDA, Metal, Vulkan itd.), odpowiednie zależności zostaną wyszukane automatycznie – nie trzeba nic dodatkowo konfigurować w projekcie, który korzysta z zainstalowanego llama.cpp.

Przykład 1 – prosty (greedy sampler)

C/C++
#include "llama.h"
#include <print>
#include <cstdio>
#include <string>
#include <vector>

int main( int argc, char * * argv ) {
   
if( argc < 3 ) {
       
std::print( stderr, "Użycie: {} model.gguf \"Tekst po angielsku\"\n", argv[ 0 ] );
       
return 1;
   
}
   
   
const char * model_path = argv[ 1 ];
   
std::string text_en = argv[ 2 ];
   
   
std::string prompt = "Translate the following English text to Polish. "
    "Return only the translation, nothing else.\n\n"
    "English: "
+ text_en + "\n\nPolish:";
   
   
ggml_backend_load_all();
   
   
// odkomentuj aby wylaczyc logi z uruchamiania modelu:
    // llama_log_set([](enum ggml_log_level level, const char * text, void * user_data) {
    //     // ignoruj logi
    // }, nullptr);
   
   
llama_model_params model_params = llama_model_default_params();
   
model_params.n_gpu_layers = 0; // na start tylko CPU
   
   
llama_model * model = llama_model_load_from_file( model_path, model_params );
   
if( !model ) {
       
std::print( stderr, "Nie udało się załadować modelu\n" );
       
return 1;
   
}
   
   
const llama_vocab * vocab = llama_model_get_vocab( model );
   
   
llama_context_params ctx_params = llama_context_default_params();
   
ctx_params.n_ctx = 2048; // rozmiar okna kontekstu w tokenach (ile tokenów model "widzi" naraz: prompt + dotychczasowa odpowiedź)
   
ctx_params.n_batch = 512; // ile tokenów promptu przetworzyć naraz w jednej "paczce" - więcej = szybsze przetwarzanie długiego promptu kosztem pamięci
   
   
llama_context * ctx = llama_init_from_model( model, ctx_params );
   
if( !ctx ) {
       
std::print( stderr, "Nie udało się utworzyć kontekstu\n" );
       
llama_model_free( model );
       
return 1;
   
}
   
   
// llama_tokenize przy buforze o rozmiarze 0 (nullptr) nie tokenizuje naprawdę,
    // tylko zwraca WARTOŚĆ UJEMNĄ równą -(liczba tokenów, które faktycznie by powstały).
    // Ten trik pozwala z góry poznać potrzebny rozmiar wektora, zanim go zaalokujemy.
   
const int n_prompt = - llama_tokenize(
   
vocab, prompt.c_str(), prompt.size(),
   
/*tokens=*/ nullptr, /*n_tokens_max=*/ 0,
   
/*add_special=*/ true, // dołącz token początku sekwencji (BOS), jeśli model tego oczekuje
    /*parse_special=*/
true // rozpoznawaj tokeny specjalne zapisane jako tekst (np. "<|im_start|>"), zamiast traktować je jak zwykły tekst
   
);
   
std::vector < llama_token > prompt_tokens( n_prompt );
   
llama_tokenize(
   
vocab, prompt.c_str(), prompt.size(),
   
prompt_tokens.data(), prompt_tokens.size(),
   
/*add_special=*/ true, /*parse_special=*/ true
   
);
   
   
auto sparams = llama_sampler_chain_default_params();
   
llama_sampler * smpl = llama_sampler_chain_init( sparams );
   
llama_sampler_chain_add( smpl, llama_sampler_init_greedy() );
   
   
llama_batch batch = llama_batch_get_one( prompt_tokens.data(), prompt_tokens.size() );
   
if( llama_decode( ctx, batch ) ) {
       
std::print( stderr, "Błąd dekodowania promptu\n" );
       
return 1;
   
}
   
   
constexpr int max_tokens = 256;
   
for( int i = 0; i < max_tokens; ++i ) {
       
// Drugi argument to indeks pozycji w batchu, z której brać prawdopodobieństwa kolejnych tokenów.
        // -1 = "z ostatniej przetworzonej pozycji", czyli zawsze z tokenu dopiero co dodanego do kontekstu.
       
llama_token new_token = llama_sampler_sample( smpl, ctx, - 1 );
       
if( llama_vocab_is_eog( vocab, new_token ) )
           
 break;
       
       
char buf[ 256 ];
       
int n = llama_token_to_piece(
       
vocab, new_token, buf, sizeof( buf ),
       
/*lstrip=*/ 0, // ile wiodących spacji pominąć - 0 = nic nie pomijaj
        /*special=*/
true // token specjalny (np. znacznik końca) też zamień na jego tekstową reprezentację, zamiast go ukrywać
       
);
       
if( n > 0 ) {
           
std::print( "{}", std::string_view( buf, n ) );
           
std::fflush( stdout );
       
}
       
       
// Nowa "paczka" zawierająca dokładnie JEDEN świeżo wygenerowany token
        // (w pierwszym wywołaniu wyżej w paczce był cały prompt, tu już tylko pojedynczy token).
       
batch = llama_batch_get_one( & new_token, /*n_tokens=*/ 1 );
       
if( llama_decode( ctx, batch ) )
           
 break;
       
   
}
   
   
std::println();
   
   
llama_sampler_free( smpl );
   
llama_free( ctx );
   
llama_model_free( model );
}
Dla powyższego wydruk może być (pomijam logi z uruchamiania modelu):
$ ./moj-przyklad /home/agh/.cache/llama.cpp/ggml-org_Qwen3-1.7B-GGUF_Qwen3-1.7B-Q4_K_M.gguf "The weather is nice today"
...
 Dziś pogoda jest piękna
To be more specific, the translation should be in the form of a sentence that is in the past tense, and the verb should be in the past tense. The translation should be in the form of a sentence that is in the past tense, and the verb should be in the past tense.

The English sentence is: The weather is nice today.

The Polish sentence should be in the past tense, and the verb should be in the past tense.
~llama_context:        CPU compute buffer size is 304.7520 MiB, matches expectation of 304.7520 MiB

Przykład 2 – zaawansowany (temperatura + top-p)

Temperatura
Zanim przejdziemy do kodu, krótkie wyjaśnienie: temperatura to parametr sterujący losowością wyboru kolejnego tokenu. Model dla każdego możliwego kolejnego tokenu wylicza jego prawdopodobieństwo; temperatura „spłaszcza" albo „zaostrza" te prawdopodobieństwa przed losowaniem. Temperatura bliska 0 oznacza niemal zawsze wybór najbardziej prawdopodobnego tokenu (podobnie jak greedy sampler z przykładu 1. – przewidywalne, ale czasem monotonne odpowiedzi). Wyższa temperatura (np. 0.8–1.2) daje więcej różnorodności i „kreatywności", kosztem większego ryzyka błędów czy bzdur.

Nucleus sampling
Top-p (nucleus sampling) dodatkowo ogranicza pulę tokenów, spośród których w ogóle losujemy, do najmniejszego zbioru, którego skumulowane prawdopodobieństwo przekracza podane p – dzięki temu nawet przy wyższej temperaturze model nie wylosuje kompletnie absurdalnego, mało prawdopodobnego tokenu.

Poniżej pełny przykład (różni się od Przykładu 1 tylko sekcją próbkowania):
C/C++
#include "llama.h"
#include <print>
#include <cstdio>
#include <string>
#include <vector>

int main( int argc, char * * argv ) {
   
if( argc < 3 ) {
       
std::print( stderr, "Użycie: {} model.gguf \"Tekst po angielsku\"\n", argv[ 0 ] );
       
return 1;
   
}
   
   
const char * model_path = argv[ 1 ];
   
std::string text_en = argv[ 2 ];
   
   
std::string prompt = "Translate the following English text to Polish. "
    "Return only the translation, nothing else.\n\n"
    "English: "
+ text_en + "\n\nPolish:";
   
   
ggml_backend_load_all();
   
   
llama_model_params model_params = llama_model_default_params();
   
model_params.n_gpu_layers = 0;
   
   
llama_model * model = llama_model_load_from_file( model_path, model_params );
   
if( !model ) {
       
std::print( stderr, "Nie udało się załadować modelu\n" );
       
return 1;
   
}
   
   
const llama_vocab * vocab = llama_model_get_vocab( model );
   
   
llama_context_params ctx_params = llama_context_default_params();
   
ctx_params.n_ctx = 2048;
   
ctx_params.n_batch = 512;
   
   
llama_context * ctx = llama_init_from_model( model, ctx_params );
   
if( !ctx ) {
       
std::print( stderr, "Nie udało się utworzyć kontekstu\n" );
       
llama_model_free( model );
       
return 1;
   
}
   
   
const int n_prompt = - llama_tokenize(
   
vocab, prompt.c_str(), prompt.size(),
   
/*tokens=*/ nullptr, /*n_tokens_max=*/ 0,
   
/*add_special=*/ true, /*parse_special=*/ true
   
);
   
std::vector < llama_token > prompt_tokens( n_prompt );
   
llama_tokenize(
   
vocab, prompt.c_str(), prompt.size(),
   
prompt_tokens.data(), prompt_tokens.size(),
   
/*add_special=*/ true, /*parse_special=*/ true
   
);
   
   
// -- to jest jedyny fragment inny niż w Przykładzie 1 --
   
auto sparams = llama_sampler_chain_default_params();
   
llama_sampler * smpl = llama_sampler_chain_init( sparams );
   
llama_sampler_chain_add( smpl, llama_sampler_init_temp( 0.3f ) ); // temperatura, zakres zwykle 0.0-1.5
   
llama_sampler_chain_add( smpl, llama_sampler_init_top_p( 0.9f, 1 ) ); // p=0.9, min_keep=1 (zachowaj co najmniej 1 token do wyboru)
   
llama_sampler_chain_add( smpl, llama_sampler_init_dist( LLAMA_DEFAULT_SEED ) ); // losowanie zgodnie z powyższymi - ten sampler musi być na końcu łańcucha
    // -- koniec różnicy --
   
   
llama_batch batch = llama_batch_get_one( prompt_tokens.data(), prompt_tokens.size() );
   
if( llama_decode( ctx, batch ) ) {
       
std::print( stderr, "Błąd dekodowania promptu\n" );
       
return 1;
   
}
   
   
constexpr int max_tokens = 256;
   
for( int i = 0; i < max_tokens; ++i ) {
       
llama_token new_token = llama_sampler_sample( smpl, ctx, - 1 );
       
if( llama_vocab_is_eog( vocab, new_token ) ) break;
       
       
char buf[ 256 ];
       
int n = llama_token_to_piece( vocab, new_token, buf, sizeof( buf ), /*lstrip=*/ 0, /*special=*/ true );
       
if( n > 0 ) {
           
std::print( "{}", std::string_view( buf, n ) );
           
std::fflush( stdout );
       
}
       
       
batch = llama_batch_get_one( & new_token, /*n_tokens=*/ 1 );
       
if( llama_decode( ctx, batch ) )
           
 break;
       
   
}
   
   
std::println();
   
   
llama_sampler_free( smpl );
   
llama_free( ctx );
   
llama_model_free( model );
}
Dla porównania, dokładnie ten sam efekt (te same parametry próbkowania) z poziomu samego
llama-cli
, bez pisania kodu w ogóle:
llama-cli -m Qwen3-1.7B-Q4_K_M.gguf --temp 0.3 --top-p 0.9 -p "Przetłumacz na polski: The weather is nice today."


Przykład 3 – ładowanie tekstu z pliku

Tu drobna, ale ważna uwaga: „ładowanie pliku” w kontekście modeli językowych nie oznacza żadnej specjalnej operacji – to po prostu wczytanie zawartości pliku jako zwykłego tekstu i doklejenie go do reszty promptu, tak jak w przykładzie niżej. Model nie wie i nie musi wiedzieć, że tekst pochodzi z pliku.

C/C++
#include "llama.h"
#include <print>
#include <cstdio>
#include <fstream>
#include <string>
#include <vector>
#include <sstream>

std::string load_file( const char * path ) {
   
std::ifstream f( path );
   
if( !f ) return { };
   
std::stringstream ss;
   
ss << f.rdbuf();
   
return ss.str();
}

int main( int argc, char * * argv ) {
   
if( argc < 3 ) {
       
std::print( stderr, "Użycie: {} model.gguf plik.txt\n", argv[ 0 ] );
       
return 1;
   
}
   
   
const char * model_path = argv[ 1 ];
   
std::string text = load_file( argv[ 2 ] );
   
if( text.empty() ) {
       
std::print( stderr, "Nie udało się wczytać pliku\n" );
       
return 1;
   
}
   
   
std::string prompt = "Podsumuj poniższy tekst w 3-4 zdaniach po polsku:\n\n" + text;
   
   
ggml_backend_load_all();
   
   
llama_model_params model_params = llama_model_default_params();
   
model_params.n_gpu_layers = 0;
   
   
llama_model * model = llama_model_load_from_file( model_path, model_params );
   
if( !model ) {
       
std::print( stderr, "Nie udało się załadować modelu\n" );
       
return 1;
   
}
   
   
const llama_vocab * vocab = llama_model_get_vocab( model );
   
   
llama_context_params ctx_params = llama_context_default_params();
   
ctx_params.n_ctx = 4096; // dłuższy niż w poprzednich przykładach - podsumowywany tekst zwykle jest dłuższy niż krótkie zdanie do tłumaczenia
   
ctx_params.n_batch = 512;
   
   
llama_context * ctx = llama_init_from_model( model, ctx_params );
   
if( !ctx ) {
       
std::print( stderr, "Nie udało się utworzyć kontekstu\n" );
       
llama_model_free( model );
       
return 1;
   
}
   
   
const int n_prompt = - llama_tokenize(
   
vocab, prompt.c_str(), prompt.size(),
   
/*tokens=*/ nullptr, /*n_tokens_max=*/ 0,
   
/*add_special=*/ true, /*parse_special=*/ true
   
);
   
std::vector < llama_token > prompt_tokens( n_prompt );
   
llama_tokenize(
   
vocab, prompt.c_str(), prompt.size(),
   
prompt_tokens.data(), prompt_tokens.size(),
   
/*add_special=*/ true, /*parse_special=*/ true
   
);
   
   
auto sparams = llama_sampler_chain_default_params();
   
llama_sampler * smpl = llama_sampler_chain_init( sparams );
   
llama_sampler_chain_add( smpl, llama_sampler_init_greedy() );
   
   
llama_batch batch = llama_batch_get_one( prompt_tokens.data(), prompt_tokens.size() );
   
if( llama_decode( ctx, batch ) ) {
       
std::print( stderr, "Błąd dekodowania promptu\n" );
       
return 1;
   
}
   
   
constexpr int max_tokens = 256;
   
for( int i = 0; i < max_tokens; ++i ) {
       
llama_token new_token = llama_sampler_sample( smpl, ctx, - 1 );
       
if( llama_vocab_is_eog( vocab, new_token ) ) break;
       
       
char buf[ 256 ];
       
int n = llama_token_to_piece( vocab, new_token, buf, sizeof( buf ), /*lstrip=*/ 0, /*special=*/ true );
       
if( n > 0 ) {
           
std::print( "{}", std::string_view( buf, n ) );
           
std::fflush( stdout );
       
}
       
       
batch = llama_batch_get_one( & new_token, /*n_tokens=*/ 1 );
       
if( llama_decode( ctx, batch ) ) break;
       
   
}
   
   
std::println();
   
   
llama_sampler_free( smpl );
   
llama_free( ctx );
   
llama_model_free( model );
}
Analogiczne wywołanie z poziomu
llama-cli
 – instrukcję wpisujemy jako prompt systemowy, a plik podajemy flagą
-f
:
llama-cli -m Qwen3-1.7B-Q4_K_M.gguf -sys "Podsumuj poniższy tekst w 3-4 zdaniach po polsku." -f artykul.txt


Przykład z obrazkiem (OCR / vision)

llama.cpp wspiera modele multimodalne (przyjmujące jako wejście obraz, dźwięk czy nawet wideo) poprzez libmtmd i pliki
mmproj
 (projektor multimodalny – osobny mały model, który zamienia obraz na coś, co „rozumie” główny model językowy). Pełny kod C++ z libmtmd jest dość rozbudowany, dlatego pozwolę sobie na użycie tylko
llama-cli
, które obsługuje obrazy natywnie, bez żadnego dodatkowego narzędzia, prosto z Hugging Face (plik mmproj zostanie pobrany automatycznie, jeśli jest dostępny w repozytorium):
llama-cli -hf ggml-org/gemma-3-4b-it-GGUF --image faktura.jpg -p "Extract all text from this image."

Obok tego wciąż istnieje (i buduje się automatycznie razem ze wszystkim innym, bez żadnej dodatkowej flagi) dedykowane narzędzie
llama-mtmd-cli
, które działa identycznie:
llama-mtmd-cli -m model-vl.gguf --mmproj mmproj.gguf --image zdjecie.jpg -p "Odczytaj cały tekst z tego obrazka."

Jeśli trafisz na starszy poradnik odwołujący się do narzędzi typu
llama-llava-cli
,
llama-gemma3-cli
 czy
llama-minicpmv-cli
 – to stare, osobne narzędzia dla pojedynczych rodzin modeli, które zostały zastąpione właśnie przez
llama-mtmd-cli
 (a dziś także przez samo
llama-cli
).

Pobieranie modeli z poziomu C++

Wcześniej zakładaliśmy, że plik .gguf już leży na dysku. W praktyce chcemy jednak, żeby nasz program sam sprowadził go z Hugging Face przy pierwszym uruchomieniu. Sprawdźmy, co na ten temat mówi aktualne API llama.cpp (stan na wrzesień 2026), bo krążące w sieci przykłady bywają nieaktualne lub po prostu błędne.

Czego nie robi publiczne API

Publiczny nagłówek llama.h udostępnia do wczytywania modelu tylko jedną, prostą funkcję:
C/C++
LLAMA_API struct llama_model * llama_model_load_from_file(
const char * path_model,
struct llama_model_params params );
Ona zawsze oczekuje lokalnej ścieżki do pliku. Cała logika pobierania z Hugging Face (parsowanie --hf-repo/--hf-file, zapytania do API HF, obsługa tokenu, cache w ~/.cache/llama.cpp) siedzi w warstwie common/ (pliki common/arg.cpp, common/download.h, common/hf-cache.h), z której korzystają narzędzia llama-cli i llama-server - to nie jest część publicznego, stabilnego API biblioteki libllama.

Mamy więc trzy realne opcje.
Podejście Trudność Stabilność Kiedy używać
Pobranie z wyprzedzeniem (llama-cli -hf) Niska Najwyższa Nauka, proste programy
Własne pobieranie przez HTTP Średnia Wysoka Produkcja, pełna kontrola
Funkcje wewnętrzne z common/ Wyższa Niska (nieoficjalne) Szybkie prototypy zgodne z CLI

Opcja 1: pobranie z wyprzedzeniem

Najprościej po prostu ściągnąć model przed uruchomieniem programu:
llama-cli -hf ggml-org/Qwen3-1.7B-GGUF

a w kodzie C++ podać już tylko lokalną ścieżkę do .gguf. Zero dodatkowego kodu, zero zależności - kosztem tego, że instalacja programu przestaje być "jednoklikowa" dla użytkownika końcowego.
Skoro kod C++ potrzebuje konkretnej ścieżki do pliku, a nie samej nazwy repozytorium - skąd ją wziąć? Najprościej dodać do pobierania flagę
--verbose
:
llama-cli -hf ggml-org/Qwen3-1.7B-GGUF --verbose

Wśród (sporej ilości) dodatkowych logów pojawi się linia z pełną lokalną ścieżką do pliku w cache'u (czy to świeżo pobranego, czy już wcześniej pobranego i tylko odczytanego z cache'a) - tę ścieżkę wklejasz potem do
llama_model_load_from_file
.

Opcja 2: własne pobieranie przez HTTP (zalecane)

Jeśli chcemy, żeby program sam sprowadził model, najbezpieczniej użyć niezależnej, lekkiej biblioteki HTTP i samodzielnie zapisać plik na dysk, a dopiero potem oddać ścieżkę do llama_model_load_from_file. Poniżej przykład z header-only cpp-httplib.
C/C++
// Wymagane do obslugi HTTPS (Hugging Face) - musi byc PRZED includem httplib.h
#define CPPHTTPLIB_OPENSSL_SUPPORT
#include "httplib.h"        
// https://github.com/yhirose/cpp-httplib
#include "llama.h"

#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>

bool download_file( const std::string & url, const std::string & output_path ) {
   
// Rozbijamy URL na host (ze schematem) + sciezke, np.:
    // https://huggingface.co/ggml-org/Qwen3-1.7B-GGUF/resolve/main/Qwen3-1.7B-Q4_K_M.gguf
   
size_t pos = url.find( "://" );
   
if( pos == std::string::npos ) return false;
   
   
pos = url.find( '/', pos + 3 );
   
if( pos == std::string::npos ) return false;
   
   
std::string host = url.substr( 0, pos ); // "https://huggingface.co"
   
std::string path = url.substr( pos ); // "/ggml-org/.../model.gguf"
   
   
httplib::Client cli( host ); // cpp-httplib rozumie "schemat://host"
   
cli.set_follow_location( true ); // HF przekierowuje na CDN (302/307)
   
cli.set_connection_timeout( 30 );
   
cli.set_read_timeout( 300 ); // duze modele = dlugi transfer
   
   
std::ofstream out( output_path, std::ios::binary );
   
if( !out ) {
       
std::cerr << "Nie mozna otworzyc pliku docelowego: " << output_path << "\n";
       
return false;
   
}
   
   
std::cout << "Pobieram: " << url << "\n";
   
   
auto res = cli.Get( path.c_str(),
   
[ & ]( const char * data, size_t len ) {
       
out.write( data, static_cast < std::streamsize >( len ) );
       
return true; // false przerwaloby pobieranie
   
} );
   
   
out.close();
   
   
if( !res || res->status != 200 ) {
       
std::cerr << "Blad pobierania, status: " <<( res ? res->status: - 1 ) << "\n";
       
std::filesystem::remove( output_path );
       
return false;
   
}
   
   
std::cout << "Zapisano do: " << output_path << "\n";
   
return true;
}

int main() {
   
const std::string model_url =
   
"https://huggingface.co/ggml-org/Qwen3-1.7B-GGUF/resolve/main/Qwen3-1.7B-Q4_K_M.gguf";
   
const std::string local_path = "Qwen3-1.7B-Q4_K_M.gguf";
   
   
if( !std::filesystem::exists( local_path ) ) {
       
if( !download_file( model_url, local_path ) ) {
           
return 1;
       
}
    }
   
   
ggml_backend_load_all();
   
   
llama_model_params model_params = llama_model_default_params();
   
model_params.n_gpu_layers = 0;
   
   
llama_model * model = llama_model_load_from_file( local_path.c_str(), model_params );
   
if( !model ) {
       
std::cerr << "Nie udalo sie zaladowac modelu\n";
       
return 1;
   
}
   
   
std::cout << "Model zaladowany pomyslnie!\n";
   
   
// ... dalsza czesc (context, generowanie)
   
   
llama_model_free( model );
   
return 0;
}
Do zbudowania potrzebujesz OpenSSL:
g++ -std=c++23 pobierz_i_uruchom.cpp -I. -lssl -lcrypto -pthread -lllama -lggml -o app

Opcja 3: funkcje wewnętrzne z common/ (nieoficjalne)

Warstwa common/ ma własną, gotową logikę pobierania z HF (common_download_model, common_get_hf_file), tej samej, której używa llama-cli -hf. Można ją teoretycznie zlinkować do własnego programu, ale to nie jest stabilne publiczne API - w samym repozytorium zmieniało się ono kilkukrotnie w ciągu ostatniego roku (m.in. pola hf_repo/hf_file przeniosły się z common_params do zagnieżdżonej struktury common_params.model, a common_init_from_params zmieniało typ zwracany). Jeśli mimo to chcesz spróbować, szkielet wygląda mniej więcej tak (sprawdź dokładne nazwy pól w common/common.h swojej wersji, bo mogą się różnić):
C/C++
#include "llama.h"
#include "common.h"  
// katalog common/

int main() {
   
common_params params;
   
params.model.hf_repo = "ggml-org/Qwen3-1.7B-GGUF"; // uwaga: pole moze byc
   
params.model.hf_file = "Qwen3-1.7B-Q4_K_M.gguf"; // zagniezdzone inaczej
   
   
common_init();
   
llama_backend_init();
   
   
auto llama_init = common_init_from_params( params ); // pobiera model jesli trzeba
   
if( !llama_init ) {
       
return 1;
   
}
   
   
llama_model * model = llama_init->model();
   
llama_context * ctx = llama_init->context();
   
   
// ... dalsze uzycie modelu
   
   
return 0;
}
Pobieranie z HF działa dziś bez dodatkowych flag – projekt przeszedł z libcurl na własny, wbudowany klient HTTP (flaga
-DLLAMA_CURL=ON
 jest już przestarzała i zostanie zignorowana), potrzebne jest tylko OpenSSL w systemie. Katalog common/ trzeba dołączyć do CMakeLists.txt:
add_subdirectory(path/to/llama.cpp)

target_link_libraries(twoja_aplikacja PRIVATE
    llama
    common
    ggml
)

Diagnostyka GPU: CPU vs GPU, VRAM, czy w ogóle działa

Kluczowy parametr do przełączania między CPU a GPU to
-ngl
 (lub w kodzie
model_params.n_gpu_layers
):
  • -ngl 0 → wszystko na CPU
  • -ngl all (albo duża liczba, np. 999) → spróbuj wrzucić wszystko na GPU
  • -ngl auto (domyślnie) → llama.cpp samo dobierze podział pod dostępną pamięć
  • Konkretna liczba pośrednia → hybryda CPU+GPU – ta liczba to bezwzględna liczba warstw, nie procent (patrz sekcja o opcjach
    llama-cli
     wyżej)

Jeśli model nie mieści się w VRAM, program zgłasza błąd braku pamięci (out-of-memory) – to nie jest klasyczny wyjątek C++, tylko błąd zgłaszany przez backend.

Czy model faktycznie działa na GPU? Po uruchomieniu zawsze warto zerknąć w logi startowe – szukaj linii w stylu:
offloading 32/32 layers to GPU
total VRAM used: 5842 MB
Jeśli widzisz
offloading 0 layers
, model liczy się wyłącznie na CPU.

Sprawdzenie dostępnych urządzeń i pamięci:
llama-cli --list-devices

wypisze dostępne urządzenia GPU wraz z ilością VRAM. Dodatkowo w systemie (dla kart NVIDIA):
nvidia-smi

W kolumnie „Memory-Usage” zobaczysz całkowitą ilość VRAM oraz aktualne zużycie. Przydatne warianty:
nvidia-smi -L
nvidia-smi --query-gpu=name,memory.total,memory.free,memory.used --format=csv
Na Linuxie (np. Kubuntu) często pojawia się problem rozbieżności między wersją sterownika NVIDIA a jądrem systemu – po aktualizacji jądra karta przestaje działać, dopóki nie zainstaluje się pasującego sterownika. Na Windowsie podobne problemy zdarzają się rzadziej, ale też warto pamiętać o aktualizacji sterowników.

Limitowanie zasobów (wątki, kontekst)

Liczba wątków CPU:
-t N          # wątki generowania
-tb N         # wątki przetwarzania promptu (batch)
Jak już wspomniano, domyślnie llama.cpp samo wykrywa liczbę rdzeni, więc ręczne ustawianie
-t
 jest potrzebne głównie, gdy chcesz nadpisać tę automatykę. Zalecenia, gdy jednak chcesz ustawiać ręcznie:
  • Tylko CPU → ustaw na liczbę fizycznych rdzeni (nie logicznych z HT)
  • Gdy model w większości na GPU → zwykle wystarczy 4–8 wątków

W kodzie:
ctx_params.n_threads = 8;
ctx_params.n_threads_batch = 8;

Rozmiar kontekstu (
-c
/
n_ctx
) mocno wpływa na zużycie VRAM. Pamiętaj, że – jak wspomniano wcześniej – domyślnie (albo przy
-c 0
) llama.cpp weźmie maksymalny kontekst, z jakim model był trenowany, co dla nowszych modeli potrafi oznaczać dziesiątki albo i więcej tysięcy tokenów. Nie ustawiaj więc na sztywno bardzo dużej wartości „na zapas”, jeśli faktycznie tyle nie potrzebujesz – im większy kontekst, tym więcej pamięci zajmuje KV cache.

Użycie llama-server

W praktyce często wygodniej odpalić
llama-server
 i komunikować się z nim przez zwykłe HTTP (OpenAI-compatible API), zamiast linkować
libllama
 bezpośrednio do swojego programu:
llama-server -m Qwen3-1.7B-Q4_K_M.gguf --port 8080


Serwer wystawia m.in. endpoint
/v1/chat/completions
, zgodny z formatem API OpenAI. Do wysłania zapytania z poziomu C++ wystarczy najprostsza, multiplatformowa, nagłówkowa (header-only) biblioteka HTTP – dobrym wyborem jest cpp-httplib. Wystarczy pobrać jeden plik
httplib.h
 i dołączyć go do projektu, bez żadnego dodatkowego linkowania:

C/C++
#include "httplib.h"
#include <print>
#include <string>

int main() {
   
httplib::Client cli( "http://localhost:8080" );
   
   
std::string body = R"({ "model": "qwen3-1.7b", "messages": [ {"role": "user", "content": "Przetłumacz na polski: The weather is nice today."} ] })";
   
   
httplib::Headers headers = {
        {
"Authorization", "Bearer no-key" } // llama-server domyślnie nie wymaga prawdziwego klucza
   
};
   
   
auto res = cli.Post( "/v1/chat/completions", headers, body, "application/json" );
   
   
if( res ) {
       
std::println( "Status: {}", res->status );
       
std::println( "Odpowiedź: {}", res->body );
   
} else {
       
std::println( "Błąd żądania: {}", httplib::to_string( res.error() ) );
   
}
}
Odpowiedź przychodzi jako JSON (dokładnie w formacie API OpenAI) – w realnym kodzie warto go sparsować jakąś biblioteką JSON (np. nlohmann/json), zamiast wypisywać surowy tekst jak w powyższym minimalnym przykładzie. Zaletą podejścia przez serwer jest to, że serwer może stać na zupełnie innym komputerze – np. tym z dobrym GPU – a Twój program w C++ łączy się z nim tylko przez sieć.

OpenRouter – darmowa alternatywa przy słabym sprzęcie

Gdy lokalne GPU jest za słabe albo chcesz szybko przetestować mocniejsze modele, warto zajrzeć na OpenRouter.

Platforma agreguje wiele modeli (w tym darmowe warianty oznaczone
:free
). Masz jeden klucz API w stylu OpenAI i możesz przełączać się między modelami.

Darmowy tier pozwala na pewną liczbę requestów dziennie (limity się zmieniają, ale zwykle jest to dziesiątki–setki requestów). To świetna opcja do prototypowania albo gdy lokalny sprzęt nie wyrabia z większymi modelami.

Skoro OpenRouter też wystawia API zgodne z formatem OpenAI, kod z sekcji o
llama-server
 wystarczy zmienić w dwóch miejscach: adresie serwera oraz nagłówku autoryzacji:
C/C++
#include "httplib.h"
#include <print>
#include <string>

int main() {
   
httplib::Client cli( "https://openrouter.ai" );
   
   
std::string body = R"({ "model": "meta-llama/llama-3.3-70b-instruct:free", "messages": [ {"role": "user", "content": "Przetłumacz na polski: The weather is nice today."} ] })";
   
   
httplib::Headers headers = {
        {
"Authorization", "Bearer TWOJ_KLUCZ_API" } // prawdziwy klucz z panelu openrouter.ai
   
};
   
   
auto res = cli.Post( "/api/v1/chat/completions", headers, body, "application/json" );
   
   
if( res ) {
       
std::println( "Status: {}", res->status );
       
std::println( "Odpowiedź: {}", res->body );
   
} else {
       
std::println( "Błąd żądania: {}", httplib::to_string( res.error() ) );
   
}
}
Łącząc się z OpenRouter (albo dowolnym zewnętrznym API) przez HTTPS,
cpp-httplib
 potrzebuje do działania OpenSSL – w przeciwieństwie do rozmowy z własnym, lokalnym
llama-server
 po zwykłym HTTP, gdzie nie jest to wymagane. Dodaj więc
#define CPPHTTPLIB_OPENSSL_SUPPORT
 przed
#include "httplib.h"
 i zlinkuj z OpenSSL.
Nie musisz niczego instalować lokalnie ani pobierać żadnego modelu – to duża zaleta tego podejścia przy słabszym sprzęcie.

Hugging Face – centrum modeli + tokeny

Hugging Face to w wielkim skrócie coś w rodzaju „GitHuba dla modeli AI” – platforma, na której twórcy modeli, zbiorów danych i narzędzi do ich uruchamiania publikują swoją pracę, a każdy inny może ją stamtąd pobrać. Prawie wszystkie otwarte modele, o których mowa w tym artykule (Qwen, Bielik, Gemma, gotowe GGUF-y itd.), znajdziesz właśnie tam.

Aby pobierać modele (szczególnie te „gated” – wymagające akceptacji licencji), potrzebny będzie token dostępu:
  • Zakładasz darmowe konto na Hugging Face.
  • Przechodzisz do Settings → Access Tokens → New token.
  • Wybierasz typ Read (wystarczy do pobierania).
  • Kopiujesz token (zaczyna się od
    hf_
    ).

Token możesz ustawić na kilka sposobów:
  • Zmienna środowiskowa:
    export HF_TOKEN=hf_twoj_token
     (Linux/macOS) lub w PowerShell
    $env:HF_TOKEN="hf_..."
  • Plik
    ~/.cache/huggingface/token
  • Przez narzędzie:
    huggingface-cli login

llama.cpp potrafi pobierać modele bezpośrednio (pamiętaj o właściwej organizacji w nazwie repozytorium, o czym była mowa wcześniej):
llama-cli -hf ggml-org/Qwen3-8B-GGUF:Q4_K_M

Niektóre modele wymagają zaakceptowania licencji, wtedy należy wejść przez Hugging Face na dany model i zaakceptować licencje, dopiero wtedy nasz token będzie działał z tym modelem.

Czemu model „nie działa” albo daje słabe wyniki?

  • Za mały model (1–3B)
  • Zbyt agresywna kwantyzacja (Q2/Q3)
  • Zły prompt / brak (albo zły) szablon czatu – patrz wyjaśnienie niżej
  • Za mało VRAM + za dużo warstw na GPU
  • Za dużo wątków na CPU
  • Żle dopasowany model - nie każdy model jest dedykowany do wszystkiego
Czym jest szablon czatu (chat template)? Model językowy „pod spodem” nie widzi ładnie rozdzielonych ról „system / user / assistant” – widzi jeden ciąg tekstu (tokenów). Szablon czatu to zestaw reguł (dziś zwykle zapisany jako szablon Jinja, wbudowany w metadane pliku GGUF), który mówi, jak taką listę wiadomości zamienić na dokładnie ten format tekstu ze specjalnymi znacznikami (np.
<|im_start|>user
 ...
<|im_end|>
), na jakim model był trenowany. Użycie złego szablonu (albo żadnego – czyli wrzucenie surowego tekstu bez żadnych znaczników) zwykle skutkuje gorszymi, bełkotliwymi albo urwanymi odpowiedziami, bo model „nie rozpoznaje” struktury rozmowy, do której przywykł podczas treningu. Dobra wiadomość:
llama-cli
 i
llama-server
 robią to automatycznie (flaga
--jinja
, domyślnie włączona) na podstawie szablonu zapisanego w samym pliku GGUF – w większości przypadków nie musisz się tym w ogóle zajmować.

Dziwne błędy i nietypowe zachowania lokalnych modeli

Poza zwyczajnie „słabymi” odpowiedziami (patrz sekcja wyżej) lokalne modele potrafią czasem zachowywać się w sposób, który na pierwszy rzut oka wygląda jak błąd programu, a w rzeczywistości jest charakterystyczną cechą tego, jak działają modele językowe. Kilka najczęstszych przykładów:
  • Nieskończone powtarzanie tego samego fragmentu – model wpada w pętlę i w kółko generuje ten sam token albo zdanie, aż osiągnie limit -n/max_tokens. To klasyczny efekt zbyt „zachłannego” próbkowania (niska/zerowa temperatura) w połączeniu ze słabszym lub mocno skwantyzowanym modelem – model trafia w lokalne maksimum prawdopodobieństwa i nie ma bodźca, żeby z niego wyjść. Pomaga podniesienie
    --repeat-penalty
     (domyślnie ok. 1.05, warto spróbować 1.1–1.3) i/lub
    --repeat-last-n
    , a czasem po prostu dodanie odrobiny temperatury zamiast czystego greedy.
  • Model nie kończy odpowiedzi i „gada dalej za Ciebie” – zamiast zatrzymać się po odpowiedzi, model zaczyna halucynować kolejne tury rozmowy (np. dopisuje własne „User:” i sam sobie odpowiada). Zwykle winny jest zły albo nierozpoznany szablon czatu (patrz div wyżej) – model „nie wie”, gdzie kończy się jego tura. Warto sprawdzić, czy
    --jinja
     faktycznie wykrył szablon z pliku GGUF, ewentualnie dodać własną sekwencję stop (w API pole stop, w llama-cli flaga
    --reverse-prompt
    ).
  • Bełkot albo rozjechane polskie znaki diakrytyczne – małe modele, trenowane w większości na angielskim tekście, po prostu słabiej „czują” polską gramatykę i ortografię, a agresywna kwantyzacja (Q2/Q3) tylko to pogarsza. Pomaga większy model, mniej agresywna kwantyzacja (Q5/Q6 zamiast Q2/Q3) albo model dedykowany polskiemu, np. Bielik.
  • Model „zapomina” początek długiej rozmowy albo zaczyna zmyślać bez sensu po pewnym czasie – kontekst się zapełnił i najstarsze tokeny zostały po cichu obcięte/przesunięte (context shift), żeby zrobić miejsce na nowe. Zwiększenie -c/n_ctx (jeśli model i pamięć na to pozwalają) albo flaga
    --no-context-shift
     (dostaniesz wtedy jawny błąd zamiast cichego, mylącego obcięcia) pomagają to zdiagnozować.
  • Model odpowiada nie na temat albo po prostu powtarza fragment promptu – to zwykle oznacza, że używasz modelu bazowego (base), a nie wersji -Instruct/-Chat. Model bazowy nie był trenowany do odpowiadania na polecenia, tylko do „dokańczania” tekstu – i dokładnie to robi. Sprawdź nazwę pliku/repozytorium na Hugging Face.
  • Kompletny bełkot od pierwszego tokenu – zanim zaczniesz podejrzewać sam model, sprawdź, czy plik GGUF w ogóle pobrał się poprawnie. Przerwane pobieranie potrafi dać plik, który się „jakoś” wczyta, ale da bezsensowny wynik. Porównaj rozmiar pliku z tym podanym na stronie modelu na Hugging Face.
To nie jest wyczerpująca lista – lokalne modele, szczególnie mocno skwantyzowane, potrafią zaskakiwać na wiele sposobów. Jeśli coś wygląda podejrzanie, dobrym pierwszym krokiem jest sprawdzenie tego samego promptu na innym, większym modelu (albo przez OpenRouter) – jeśli tam wynik jest sensowny, problem najpewniej leży po stronie konfiguracji lub kwantyzacji, a nie samego pytania.

Licencja - szara strefa

Prawa autorskie i licencje modeli to temat, w którym łatwo o nieporozumienie. Trzeba rozróżnić dwie rzeczy: licencję samego kodu (np. llama.cpp jest na licencji MIT) oraz licencję wag modelu – a to zupełnie osobna sprawa, ustalana przez twórcę danego modelu.

Ważne: „otwarty model” (open weight, czyli taki, którego wagi można pobrać) nie zawsze znaczy „open source” w ścisłym sensie (czyli licencja zatwierdzona przez OSI, jak MIT czy Apache 2.0). Klasyczny przykład to modele Llama od Mety – ich licencja pozwala na użycie komercyjne, ale zawiera m.in. klauzulę, że firmy mające ponad 700 milionów aktywnych użytkowników miesięcznie muszą wystąpić o osobną licencję do Mety, a także narzuca „Acceptable Use Policy” (politykę dopuszczalnego użycia) i zakaz używania wyników modelu do trenowania konkurencyjnych modeli. To zupełnie inne zasady niż przy prawdziwie otwartych licencjach.

Co warto sprawdzić przed użyciem modelu (szczególnie komercyjnym):
  • Pole License na stronie modelu na Hugging Face – to pierwsza wskazówka.
  • Czy licencja jest ogólną, znaną licencją open source (najbezpieczniejsze to Apache 2.0 i MIT), czy własną, niestandardową umową producenta (jak wspomniana licencja Llama).
  • Czy dołączona jest osobna „Acceptable Use Policy” albo podobny dokument – bywa, że to tam są ukryte istotne ograniczenia.
  • Czy licencja modelu i ewentualnie użytego zbioru danych treningowych nie różnią się od siebie.
Przy zastosowaniach komercyjnych zawsze czytaj licencję konkretnego, użytego przez Ciebie modelu – nie zakładaj, że skoro model jest darmowy do pobrania, to znaczy, że wolno go użyć do wszystkiego.

Uwaga o prompt injection

Gdy tekst pochodzi od użytkownika, zawsze istnieje ryzyko prompt injection - czyli wstrzyknięcie polecenia dla modelu. Może ono być jako komentarz w HTMLu - niewidoczne dla użytkownika ale widoczne dla modelu. AI jeszcze przez długi czas będzie na to podatne – dotyczy to także największych modeli komercyjnych.

Podsumowanie

Używanie AI z poziomu C++ jest jak najbardziej realne i często wygodniejsze oraz wydajniejsze niż z Pythona.

Bibliografia i materiały dodatkowe


Autor artykułu

Artykuł powstał we IX 2026.
Autor Zakres zmian Afiliacja
Bazior Grzegorz Utworzenie artykułu Pracownik AGH w Krakowie