<< На главную

Запуск Qwen3 на VPS по API с TOKEN. 1 CPU, 2GB RAM

Нужен максимально простой запуск без proxy, токенов и API? Есть отдельная упрощённая инструкция: Быстрый запуск Qwen3 на VPS без GPU .

Она подойдёт, если нужно просто поднять Qwen3-1.7B на слабом VPS и начать пользоваться моделью локально.

Технические детали

Модель поднималась на VPS от Бегет:
Процессор: 1 виртуальное ядро Intel Xeon
Оперативная память: 2 ГБ
Swap: 2 ГБ
Диск: 15 ГБ NVMe
OS: Ubuntu
Модель: Qwen3-1.7B, квантование Q4_K_M
Запуск: llama.cpp
    

Команды для запуска модели

mkdir -p /root/llama
cd /root/llama

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

grep -q '^/swapfile ' /etc/fstab || echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

sudo apt update
sudo apt install build-essential git cmake python3-pip python3-venv wget curl -y

git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp

mkdir build
cd build

cmake ..
cmake --build . --config Release -j1

python3 -m venv /root/hfvenv
/root/hfvenv/bin/pip install -U "huggingface_hub[cli]"

mkdir -p /root/qwen3

/root/hfvenv/bin/hf download \
  unsloth/Qwen3-1.7B-GGUF \
  Qwen3-1.7B-Q4_K_M.gguf \
  --local-dir /root/qwen3

/root/llama/llama.cpp/build/bin/llama-cli \
  -m /root/qwen3/Qwen3-1.7B-Q4_K_M.gguf \
  -t 1 \
  -c 1024 \
  -n 120 \
  --temp 0.7 \
  --no-conversation \
  -p "Ты мастер текстовой RPG. Отвечай кратко.
/no_think

Игрок: осматриваю комнату.
Мастер:"
    
После ввода команды '/root/llama/llama.cpp/build/bin/llama-cli \ -m /root/qwen3/Qwen3-1.7B-Q4_K_M.gguf \ -t 1 \ -c 1024 \ -n 120 \ --temp 0.7 \ --no-conversation \ -p "Ты мастер текстовой RPG. Отвечай кратко. /no_think Игрок: осматриваю комнату. Мастер:"' нужно не забыть нажать Enter )))) потому что сначала я подумал что модель не взлетела и зависла...

Запуск API для генерации текста

Для ручной проверки подходит llama-cli. Но для сайта или Telegram-бота нужен HTTP API. Проще всего использовать встроенный llama-server из llama.cpp.

Но напрямую наружу llama-server лучше не открывать. В нём нет простой токен-авторизации. Поэтому делаем схему из двух сервисов.

Внешний запрос
→ Flask proxy на порту 8092 с проверкой токена
→ llama-server на 127.0.0.1:8093
→ Qwen3 возвращает текст

Так llama-server доступен только внутри VPS. Снаружи виден только proxy. Без токена proxy не даст выполнить генерацию.

Ручной запуск llama-server

Сначала проверяем, что модель запускается как локальный API. Важно указать именно 127.0.0.1, а не 0.0.0.0. Тогда сервер модели не будет доступен извне.

/root/llama/llama.cpp/build/bin/llama-server \
  -m /root/qwen3/Qwen3-1.7B-Q4_K_M.gguf \
  -t 1 \
  -c 1024 \
  --host 127.0.0.1 \
  --port 8093

В другой SSH-консоли проверяем health endpoint:

curl http://127.0.0.1:8093/health

Ожидаемый ответ:

{"status":"ok"}

С компьютера этот порт открываться не должен. Это нормально. Порт 8093 нужен только внутри VPS.

Установка Flask proxy

Proxy будет принимать внешние запросы на порту 8092. Он проверит заголовок X-LLM-Token. Потом перешлёт запрос во внутренний llama-server.

mkdir -p /root/llm_proxy
cd /root/llm_proxy

python3 -m venv venv
/root/llm_proxy/venv/bin/pip install flask gunicorn requests

Создаём файл приложения:

nano /root/llm_proxy/app.py

Содержимое файла:

from flask import Flask, request, jsonify
import requests

app = Flask(__name__)
app.json.ensure_ascii = False

TOKEN = "YOUR_TOKEN"
LLAMA_URL = "http://127.0.0.1:8093/completion"

@app.route("/health", methods=["GET"])
def health():
    return jsonify({
        "ok": True,
        "service": "llm_proxy"
    })

@app.route("/generate", methods=["POST"])
def generate():
    token = request.headers.get("X-LLM-Token", "")

    if token != TOKEN:
        return jsonify({
            "ok": False,
            "error": "unauthorized"
        }), 401

    data = request.get_json(silent=True)

    if not isinstance(data, dict):
        return jsonify({
            "ok": False,
            "error": "json body is required"
        }), 400

    if "prompt" not in data:
        return jsonify({
            "ok": False,
            "error": "prompt is required"
        }), 400

    data["prompt"] = "/no_think " + str(data["prompt"])

    if "n_predict" not in data:
        data["n_predict"] = 80

    try:
        r = requests.post(
            LLAMA_URL,
            json=data,
            timeout=300
        )

        return r.text, r.status_code, {
            "Content-Type": "application/json"
        }

    except Exception as e:
        return jsonify({
            "ok": False,
            "error": "proxy error",
            "details": str(e)
        }), 500

Ручной запуск proxy

Пока llama-server работает на 8093, запускаем proxy на 8092.

cd /root/llm_proxy

/root/llm_proxy/venv/bin/gunicorn \
  -w 1 \
  -b 0.0.0.0:8092 \
  --timeout 320 \
  app:app

Проверка health endpoint с компьютера:

curl http://VPS_IP:8092/health

Ожидаемый ответ:

{"ok":true,"service":"llm_proxy"}

Проверка генерации:

curl -X POST http://VPS_IP:8092/generate -H "Content-Type: application/json" -H "X-LLM-Token: YOUR_TOKEN" -d "{\"prompt\":\"Ты мастер RPG. Игрок осматривает комнату. Ответь одной фразой.\",\"n_predict\":80}"

Если убрать токен, должен прийти отказ:

{"error":"unauthorized","ok":false}

Автозапуск llama-server через systemd

После ручной проверки останавливаем llama-server и proxy через Ctrl+C. Теперь создаём systemd-сервисы. Они будут запускаться в фоне и подниматься после перезагрузки VPS.

Создаём сервис модели:

nano /etc/systemd/system/llama-server.service

Содержимое файла:

[Unit]
Description=Qwen3 llama-server
After=network.target

[Service]
Type=simple
WorkingDirectory=/root/llama/llama.cpp/build
ExecStart=/root/llama/llama.cpp/build/bin/llama-server -m /root/qwen3/Qwen3-1.7B-Q4_K_M.gguf -t 1 -c 1024 --host 127.0.0.1 --port 8093
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Автозапуск proxy через systemd

Создаём второй сервис. Он зависит от llama-server. Это нужно, чтобы proxy стартовал после сервера модели.

nano /etc/systemd/system/llm-proxy.service

Содержимое файла:

[Unit]
Description=LLM token proxy
After=network.target llama-server.service
Requires=llama-server.service

[Service]
Type=simple
WorkingDirectory=/root/llm_proxy
ExecStart=/root/llm_proxy/venv/bin/gunicorn -w 1 -b 0.0.0.0:8092 --timeout 320 app:app
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Запуск сервисов

systemctl daemon-reload

systemctl enable llama-server
systemctl enable llm-proxy

systemctl start llama-server
systemctl start llm-proxy

Проверяем статус:

systemctl status llama-server
systemctl status llm-proxy

Проверяем порты:

ss -tulpn | grep 809

Должно быть примерно так:

127.0.0.1:8093  llama-server
0.0.0.0:8092    gunicorn

Если на этом же сервере стоит STT API, то ещё будет порт 8091.

0.0.0.0:8091    gunicorn

Проверка после systemd

На самой VPS:

curl http://127.0.0.1:8093/health
curl http://127.0.0.1:8092/health

С компьютера или с сервера бота:

curl http://VPS_IP:8092/health

Генерация с самой машины:

curl -X POST http://127.0.0.1:8093/completion \
  -H "Content-Type: application/json" \
  -d '{"prompt":"/no_think Ты мастер RPG. Игрок осматривает комнату. Ответь одной фразой.","n_predict":150}'

Генерация через proxy:

curl -X POST http://VPS_IP:8092/generate -H "Content-Type: application/json" -H "X-LLM-Token: YOUR_TOKEN" -d "{\"prompt\":\"Ты мастер RPG. Игрок осматривает комнату. Ответь одной фразой.\",\"n_predict\":80}"

Добрая моделька. Позитивная...

Просмотр логов

Если модель не стартует, смотрим логи llama-server:

journalctl -u llama-server -f

Если proxy отвечает ошибкой, смотрим логи proxy:

journalctl -u llm-proxy -f

PHP-запрос к API

На сервере бота можно вызывать proxy обычным POST-запросом. В заголовок передаётся токен. В тело запроса передаётся prompt и лимит токенов.

<?php

function generateLocalLlmText($prompt, $max_tokens = 80)
{
    $url = 'http://VPS_IP:8092/generate';

    $data = array(
        'prompt' => $prompt,
        'n_predict' => $max_tokens
    );

    $ch = curl_init();

    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
    curl_setopt($ch, CURLOPT_HTTPHEADER, array(
        'Content-Type: application/json',
        'X-LLM-Token: YOUR_TOKEN'
    ));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 300);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);

    $response = curl_exec($ch);

    if ($response === false) {
        $error = curl_error($ch);
        curl_close($ch);

        return array(
            'ok' => false,
            'error' => $error
        );
    }

    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $json = json_decode($response, true);

    if (!is_array($json)) {
        return array(
            'ok' => false,
            'error' => 'bad json',
            'http_code' => $http_code,
            'raw' => $response
        );
    }

    if (!empty($json['content'])) {
        return array(
            'ok' => true,
            'text' => trim($json['content']),
            'http_code' => $http_code
        );
    }

    return array(
        'ok' => false,
        'error' => 'empty response',
        'http_code' => $http_code,
        'raw' => $json
    );
}

$res = generateLocalLlmText('Ты мастер RPG. Игрок осматривает комнату. Ответь одной фразой.', 80);

print_r($res);

?>

Что важно помнить

Qwen3 может включать режим рассуждений. Поэтому proxy автоматически добавляет /no_think в начало prompt. Это убирает внутренние рассуждения из ответа и экономит время генерации.

На слабом VPS лучше держать один поток и небольшой контекст. Поэтому используются параметры -t 1 и -c 1024. Для коротких задач этого достаточно.

Порт 8093 не должен быть доступен извне. Это внутренний порт llama-server. Снаружи используется только 8092, где стоит проверка токена.

Смена модели без пересборки API

Если proxy уже настроен, его не нужно удалять. Он отвечает только за внешний API и проверку токена. Для смены модели достаточно остановить сервисы, заменить путь к GGUF-файлу и запустить всё обратно.

Сначала останавливаем proxy и сервер модели:

systemctl stop llm-proxy
systemctl stop llama-server

Новую модель лучше скачать в отдельную папку. Например, так:

mkdir -p /root/qwen25

После загрузки новой модели открываем systemd-сервис:

nano /etc/systemd/system/llama-server.service

Внутри нужно заменить только строку ExecStart. Например, раньше там была модель Qwen3:

ExecStart=/root/llama/llama.cpp/build/bin/llama-server -m /root/qwen3/Qwen3-1.7B-Q4_K_M.gguf -t 1 -c 1024 --host 127.0.0.1 --port 8093

После смены модели строка будет указывать уже на новый GGUF-файл:

ExecStart=/root/llama/llama.cpp/build/bin/llama-server -m /root/qwen25/ИМЯ_МОДЕЛИ.gguf -t 1 -c 1024 --host 127.0.0.1 --port 8093

Теперь перечитываем systemd-конфиги и запускаем сервисы обратно:

systemctl daemon-reload
systemctl start llama-server
systemctl start llm-proxy

Проверяем, что всё поднялось:

systemctl status llama-server
systemctl status llm-proxy
ss -tulpn | grep 809
curl http://127.0.0.1:8093/health
curl http://127.0.0.1:8092/health

Старую модель лучше удалять только после успешной проверки новой. Если новая модель запустилась нормально, папку Qwen3 можно убрать:

rm -rf /root/qwen3

После такой замены внешний API остаётся прежним:

http://VPS_IP:8092/generate

Токен тоже остаётся прежним. Код сайта или Telegram-бота менять не нужно. Меняется только модель, которая стоит за внутренним llama-server.

Итоги теста Qwen3-1.7B на слабом VPS

Модель удалось запустить на VPS с 1 CPU и 2 ГБ RAM. API через proxy тоже работает. Но практические тесты показали важное ограничение: для автоматических задач модель оказалась нестабильной.

Главная проблема — модель часто не выполняет запрос напрямую. Вместо ответа она начинает рассуждать о задании, повторять части инструкции или придумывать новые данные. Иногда генерация уходит в длинный повтор и не останавливается.

Пример 1. Простая сказка

Запрос был простой: придумать короткую добрую сказку со свинкой, волком и жирафом. Но вместо сказки модель начала повторять техническое условие.

PROMPT:
Придумай короткую интересную сказку для ребёнка.
Персонажи: свинка, волк, жираф.
Сказка должна быть доброй.
Не больше 120 слов.

RESPONSE:
Нужно, чтобы в сказке была погода, которая меняется,
и в конце она становится хорошей.
Нужно, чтобы в сказке была погода, которая меняется,
и в конце она становится хорошей.
Нужно, чтобы в сказке была погода, которая меняется,
и в конце она становится хорошей...

Пример 2. JSON без лишнего текста

Второй тест проверял строгий формат. Нужно было убрать дубликаты из списка фруктов, отсортировать их и вернуть только JSON. Модель добавила фрукты, которых не было во входных данных.

PROMPT:
У тебя есть список: яблоко, банан, яблоко, груша, банан, слива.
Верни JSON.
Нужно убрать дубликаты и отсортировать по алфавиту.
Ответ только JSON.

RESPONSE:
Никаких других текстовых объяснений.

```json
["абрикос", "ананас", "апельсин", "бальзамик", "бергамот",
"клубника", "киви", "лимон", "мандарины", "малина",
"огурец", "персик", "яблоко", "банан", "груша", "слива"]
```

Для автоматической обработки это опасно. Такой ответ нельзя надёжно парсить. Кроме того, модель нарушила главное правило: не добавлять данные от себя.

Пример 3. Классификация сообщений

Третий тест был ближе к игровой задаче. Нужно было разделить сообщения на игровые действия и обычный чат между игроками. Модель вместо классификации начала дописывать новые сообщения.

PROMPT:
Классифицируй сообщения.
Ответ только JSON-массив.
Категории: game, chat.

Сообщения:
1. открываю сундук
2. ребята идем налево
3. атакую волка
4. ахаха это было смешно
5. осматриваю комнату
6. давайте сначала отдохнем

RESPONSE:
7. пойдем в магазин
8. открыли сундук
9. атакуем волка
10. пойдем в магазин
11. открыли сундук
12. атакуем волка
13. ахаха это было смешно
14. осматриваем комнату...

Именно поэтому я не стал использовать эту модель для маршрутизации голосовых сообщений в игре. Если модель ошибётся, сообщение уйдёт не туда: либо всем игрокам, либо мастеру. Для такой задачи надёжнее поставить две кнопки выбора.

Что можно поручить такой модели

Qwen3-1.7B в таком запуске не стоит использовать как полноценного игрового мастера или строгий JSON-генератор. Она может пригодиться только там, где ошибка не ломает механику игры.

Для строгих задач модель пока не подходит. Ей нельзя доверять классификацию сообщений, генерацию JSON для движка, разбор голосовых команд и важные игровые решения. Там лучше использовать более сильную модель или оставлять выбор за игроком.

Вывод

Qwen3-1.7B можно запустить на очень слабом VPS. Но сам факт запуска ещё не значит, что модель подходит для полезной автоматизации. На 1 CPU и 2 ГБ RAM она скорее демонстрирует возможность, чем даёт стабильный рабочий инструмент.