TL;DRМы пытались запустить LLM inference на старой AMD RX580 (8 VRAM) через ROCm в Kubernetes. GPU корректно определялся, VRAM использовалась, но inference падаTL;DRМы пытались запустить LLM inference на старой AMD RX580 (8 VRAM) через ROCm в Kubernetes. GPU корректно определялся, VRAM использовалась, но inference пада

Запускаем LLM на AMD RX580: разбор проблем ROCm, Ollama и реальный GPU inference

2026/03/15 11:15
10м. чтение
Для обратной связи или замечаний по поводу данного контента, свяжитесь с нами по адресу [email protected]
TL;DR

Мы пытались запустить LLM inference на старой AMD RX580 (8 VRAM) через ROCm в Kubernetes. GPU корректно определялся, VRAM использовалась, но inference падал с ошибками вида:

hipMemGetInfo(free, total) CUDA error: invalid argument

После серии экспериментов с ROCm userspace, Docker‑образами и Kubernetes deployment выяснилось, что проблема лежит на границе:

kernel → ROCm runtime → ggml backend

Финальное решение включало:

  • переход на kernel 6.8

  • стабилизацию ROCm runtime

  • использование llama.cpp + ROCm

  • grammar‑constrained decoding для strict sanity prompts

В итоге мы получили стабильный GPU inference:

  • ~42 токен/сек

  • gpu_busy_percent → до 100%

на обычной RX580.

Введение

Большинство гайдов по запуску LLM предполагают NVIDIA GPU и CUDA. Если у вас AMD — особенно старая карта вроде RX580 — готовьтесь к расследованию.

Большинство примеров и гайдов ориентированы на NVIDIA:

  • CUDA

  • TensorRT

  • готовые контейнеры и helm-чарты

С AMD всё сложнее. Основная экосистема строится вокруг ROCm, который:

  • Официально поддерживает ограниченный набор GPU, особенно старых, особенно старый ROCm

  • Часто имеет несовместимости на границе kernel / userspace

  • Хуже документирован для старых карт

При этом RX580 — одна из самых распространённых видеокарт:

  • дешёвая на вторичном рынке

  • 8GB VRAM

  • достаточная для небольших LLM

Контекст и цель

Задача была прикладной: получить стабильный GPU inference на AMD RX580 (gfx803) в Kubernetes-контуре. Казалось что задачу получится решить дефолтным образом, но...

.. на практике упёрлись в ограничения совместимости.

Образ rocm/llama.cpp:llama.cpp-b6652.amd0_rocm7.0.0_ubuntu24.04_server даже не увидел gfx803. Workaround через HSA_OVERRIDE_GFX_VERSION не помог

ggml_cuda_init: failed to initialize ROCm: no ROCm-capable device is detected

Чтобы исключить догадки, диагностику вели послойно:

  • Helm/Argo-манифесты и корректность владения GPU через device plugin.системные и контейнерные логи;

  • runtime-ошибки (hipMemGetInfo, loader failure, деградация качества до gibberish-output);

  • GPU-метрики (gpu_busy_percent, VRAM, температура, частоты);

Первая ипотеза: проблема в ROCm userspace

Мы предположили, что проблема может быть в userspace‑части ROCm. Попробовали альтернативный вариант - взять более "готовый" образ из гитхаба woodrex83/ROCm-For-RX580

GPU корректно определился:

library=rocm
compute=gfx803

Но ошибка hipMemGetInfo никуда не исчезла. Оно и понятно, поддержка этого семейства видеокарт прекратилась в ROCm 4.5

Поднимаем Ollama на ROCm

Первый шаг — убедиться, что контейнер видит GPU. В Kubernetes доступ к девайсам обеспечил AMD GPU Operator, в докере для дебага нужно смонтировать /dev/kfd,/dev/dri

Запускаем

docker run -d \ --device /dev/kfd \ --device /dev/dri \ --group-add video \ -e HSA_OVERRIDE_GFX_VERSION=8.0.3 \ ollama:v0.1.24-rocm431

В логах Ollama мы увидели:

library=rocm compute=gfx803 name=1002:67df

Это означало, что ROCm успешно обнаружил GPU.

Вторая проблема проблема: GPU есть, inference падает

При запуске модели:

ollama run tinylama

Появлялась ошибка CUDA error: invalid argument hipMemGetInfo(free, total)

Интересно, что при этом:

  • pod был healthy

  • API отвечал

  • VRAM резервировалась

На первый взгляд система выглядела рабочей. Но inference либо падал, либо выдавал мусор. Это уже четко указывало на runtime-цепочку

kernel -> ROCm runtime -> ggml backend.

Следующая гипотеза: ggml runtime

Следующим подозреваемым стал runtime внутри inference backend.

Ollama использует ggml, который взаимодействует с ROCm через HIP. Но на этом этапе было непонятно — проблема в runtime или в устаревшем железе

Vulkan как диагностический инструмент

Чтобы проверить гипотезу, мы попробовали альтернативный backend llama.cpp + Vulkan

docker pull ghcr.io/ggml-org/llama.cpp:full-vulkan docker run --rm -it \ --privileged \ --device /dev/dri:/dev/dri \ -v /home/user/models:/models:ro \ --entrypoint /app/llama-cli \ ghcr.io/ggml-org/llama.cpp:full-vulkan \ -m /models/tiny.gguf \ -ngl 999 \ -n 128 \ -p "Write a long detailed story about space exploration."

Результат оказался неожиданным. Inference заработал с первого запуска. Это означало:

  1. GPU исправен

  2. Модель работает

  3. gglm не причем

Vulkan подтвердил, что проблема не в железе. После этого мы вернулись к ROCm и начали искать системные несовместимости.
Vulkan подтвердил, что проблема не в железе. После этого мы вернулись к ROCm и начали искать системные несовместимости.

Проверка userspace-образов ROCm и эксперименты с kernel

Мы проверили еще несколько вариантов userspace:

  • GPU по-прежнему детектился;

  • класс ошибок hipMemGetInfo не исчезал полностью;

  • часть симптомов менялась, но root cause не уходил.

Хост работал на: kernel 5.15.0-171

Симптомы:

  • ROCm видел GPU

  • runtime иногда падал при старте

Мы попробовали более новое ядро: 6.8.0–101 как проверка одной из гипотез совместимости версий kernel ↔ ROCm userspace ↔ ggml

После перезагрузки поведение изменилось радикально. Модель начала стабильно загружаться и выдавать токены.

Grammar‑constrained decoding

После стабилизации ROCm осталось узкое место: strict sanity-промпты вида:

  • Reply with exactly hi

  • What is 1+1? Reply with exactly 2

  • Say only the number 7

В обычном unconstrained-режиме модель не всегда отвечала точно (Hello, 1, The, а то и ##### или G G G) — это была проблема декодирования/формата, а не GPU runtime.

Чтобы закрыть кейс, добавили грамматики на уровне декодера:

  • для hi: root ::= «hi»

  • для 2: root ::= «2»

  • для 7: root ::= «7»

И включили их в запросы к llama-server. Заработало. Это хороший диагностический инструмент, но плохой production-режим для обычной генерации.

После Grammar: снятие костыля и переход на нормальный профиль

Grammar жёстко ограничивает декодер и «прячет» часть поведенческих проблем, поэтому мы использовали его как контроль, а затем вернулись к unconstrained-декодингу и стабилизировали качество настройками.

Финальный профиль. Цель: убрать «почти правильные» ответы и снизить дрейф генерации без искусственных ограничений.

--n-gpu-layers 999 --ctx-size 2048 --batch-size 512 --ubatch-size 128 temperature=0 top_p=1 top_k=1 min_p=0 repeat_penalty=1.05 max_tokens=256-1024 #для рабочих запросов

Мы сделали декодинг максимально детерминированным, чтобы качество определялось моделью и runtime, а не случайностью сэмплинга.
Для API-сценариев со строгим парсингом используемjson schema (где это поддерживается).
Итого - строгие форматы решаются на уровне контракта ответа, а не форсированием каждого токена грамматикой.

Путаница с метриками (3 экспортера + CPU fallback)

Проблема была не только в inference, но и в наблюдаемости.

Мы одновременно работали с тремя источниками:

  1. default-metrics-exporter (от AMD GPU Operator - изначально показался наиболее логичным и prod-ready)

  2. radeon-exporter ( kmulvey/radeon_exporter:latest - в итоге именно он неизменно отдавал хоть и мало, но точных метрик)

  3. Грубый fallback на прямое чтение sysfs (/sys/class/drm/card*/device/gpu_busy_percent)

Вот такой дэшборд собрали перепробовав 3 разных экспортера
Вот такой дэшборд собрали перепробовав 3 разных экспортера

Cкрипт для настройки вентиляторов

Также выяснилось что стандартный драйвер видеокарты поддерживает управление вентиляторами только auto/manual

  • На auto температура улетала в небеса

  • На manual только фиксированное значение оборотов

-> Пользуемся знаниями теории управления и пишем простой PID-регулятор оборотов

Ниже минимальный script для ручного fan-control

#!/usr/bin/env python3 import glob import os import signal import sys import time from dataclasses import dataclass @dataclass class Config: # Цель по температуре target_temp_c: float = 45.0 # Температурные зоны idle_temp_c: float = 42.0 warm_temp_c: float = 48.0 hot_temp_c: float = 55.0 very_hot_temp_c: float = 68.0 emergency_temp_c: float = 75.0 # PWM границы min_pwm: int = 88 idle_pwm: int = 95 base_pwm: int = 108 max_pwm: int = 255 emergency_pwm: int = 255 # PID-подобные коэффициенты kp: float = 7.0 ki: float = 0.05 kd: float = 14.0 # Упреждающая реакция на загрузку GPU busy_gain: float = 0.8 busy_threshold: float = 5.0 # Поведение interval_sec: float = 2.0 hysteresis_c: float = 0.5 # Ограничение изменения PWM за шаг max_pwm_step_up: int = 16 max_pwm_step_down: int = 8 # Чтобы не дёргать ШИМ по мелочи min_effective_pwm_delta: int = 2 # Антивиндап integral_min: float = -250.0 integral_max: float = 350.0 # Если очень холодно и GPU почти не занят very_cool_temp_c: float = 38.0 very_cool_busy_max: float = 10.0 # Усиление реакции в горячих зонах hot_zone_boost_pwm: int = 12 very_hot_zone_boost_pwm: int = 28 class AmdGpuFanController: def __init__(self, cfg: Config): self.cfg = cfg self.hwmon_path = self._find_hwmon() self.card_path = "/sys/class/drm/card0/device" self.temp_path = os.path.join(self.hwmon_path, "temp1_input") self.pwm_path = os.path.join(self.hwmon_path, "pwm1") self.pwm_enable_path = os.path.join(self.hwmon_path, "pwm1_enable") self.fan_rpm_path = os.path.join(self.hwmon_path, "fan1_input") self.gpu_busy_path = os.path.join(self.card_path, "gpu_busy_percent") self.integral = 0.0 self.last_temp_c = None self.last_busy = None self.last_pwm = None self.running = True def _find_hwmon(self) -> str: # сначала старый путь (иногда используется) matches = glob.glob("/sys/class/drm/card0/device/hwmon/hwmon*") if matches: return matches[0] # стандартный путь через /sys/class/hwmon for path in glob.glob("/sys/class/hwmon/hwmon*"): try: with open(os.path.join(path, "name")) as f: if f.read().strip() == "amdgpu": return path except Exception: pass raise RuntimeError("amdgpu hwmon device not found") def _read_int(self, path: str, default: int = 0) -> int: try: with open(path, "r") as f: return int(f.read().strip()) except Exception: return default def _write_int(self, path: str, value: int) -> None: with open(path, "w") as f: f.write(str(value)) def read_temp_c(self) -> float: return self._read_int(self.temp_path) / 1000.0 def read_pwm(self) -> int: return self._read_int(self.pwm_path) def read_rpm(self) -> int: return self._read_int(self.fan_rpm_path, default=-1) def read_gpu_busy(self) -> float: return float(self._read_int(self.gpu_busy_path, default=0)) def set_manual_mode(self) -> None: self._write_int(self.pwm_enable_path, 1) def set_auto_mode(self) -> None: self._write_int(self.pwm_enable_path, 2) def set_pwm(self, pwm: int) -> None: pwm = max(self.cfg.min_pwm, min(self.cfg.max_pwm, int(round(pwm)))) self._write_int(self.pwm_path, pwm) self.last_pwm = pwm @staticmethod def clamp(value: float, lo: float, hi: float) -> float: return max(lo, min(hi, value)) def rate_limit_pwm(self, target_pwm: int) -> int: if self.last_pwm is None: return target_pwm if target_pwm > self.last_pwm: return min(target_pwm, self.last_pwm + self.cfg.max_pwm_step_up) return max(target_pwm, self.last_pwm - self.cfg.max_pwm_step_down) def compute_target_pwm(self, temp_c: float, busy: float, dtemp_dt: float) -> int: if temp_c >= self.cfg.emergency_temp_c: self.integral = 0.0 return self.cfg.emergency_pwm if temp_c <= self.cfg.very_cool_temp_c and busy <= self.cfg.very_cool_busy_max: self.integral *= 0.85 return self.cfg.idle_pwm error = temp_c - self.cfg.target_temp_c effective_error = 0.0 if abs(error) < self.cfg.hysteresis_c else error self.integral += effective_error * self.cfg.interval_sec self.integral = self.clamp( self.integral, self.cfg.integral_min, self.cfg.integral_max, ) busy_term = 0.0 if busy > self.cfg.busy_threshold: busy_term = (busy - self.cfg.busy_threshold) * self.cfg.busy_gain pwm = ( self.cfg.base_pwm + self.cfg.kp * effective_error + self.cfg.ki * self.integral + self.cfg.kd * dtemp_dt + busy_term ) if temp_c >= self.cfg.hot_temp_c: pwm += self.cfg.hot_zone_boost_pwm if temp_c >= self.cfg.very_hot_temp_c: pwm += self.cfg.very_hot_zone_boost_pwm if temp_c >= self.cfg.warm_temp_c and busy >= 40: pwm = max(pwm, self.cfg.base_pwm + 18) if temp_c <= self.cfg.idle_temp_c and busy < 20: pwm = min(pwm, self.cfg.idle_pwm + 8) return int(round(self.clamp(pwm, self.cfg.min_pwm, self.cfg.max_pwm))) def control_step(self) -> None: temp_c = self.read_temp_c() rpm = self.read_rpm() busy = self.read_gpu_busy() if self.last_pwm is None: self.last_pwm = self.read_pwm() if self.last_temp_c is None: dtemp_dt = 0.0 else: dtemp_dt = (temp_c - self.last_temp_c) / self.cfg.interval_sec target_pwm = self.compute_target_pwm(temp_c=temp_c, busy=busy, dtemp_dt=dtemp_dt) limited_pwm = self.rate_limit_pwm(target_pwm) if self.last_pwm is None or abs(limited_pwm - self.last_pwm) >= self.cfg.min_effective_pwm_delta: self.set_pwm(limited_pwm) else: limited_pwm = self.last_pwm error = temp_c - self.cfg.target_temp_c print( f"temp={temp_c:5.1f}C " f"busy={busy:5.1f}% " f"rpm={rpm:4d} " f"err={error:+5.1f} " f"dT/dt={dtemp_dt:+5.2f}C/s " f"int={self.integral:+7.1f} " f"pwm={limited_pwm:3d}", flush=True, ) self.last_temp_c = temp_c self.last_busy = busy def run(self) -> None: self.set_manual_mode() if self.last_pwm is None: try: self.last_pwm = self.read_pwm() except Exception: self.last_pwm = self.cfg.base_pwm self.set_pwm(self.last_pwm) print(f"Using hwmon path: {self.hwmon_path}") print("Manual fan control enabled.") print( f"Target={self.cfg.target_temp_c}C, " f"idle={self.cfg.idle_temp_c}C, " f"warm={self.cfg.warm_temp_c}C, " f"hot={self.cfg.hot_temp_c}C, " f"very_hot={self.cfg.very_hot_temp_c}C, " f"emergency={self.cfg.emergency_temp_c}C", flush=True, ) while self.running: self.control_step() time.sleep(self.cfg.interval_sec) def stop(self, restore_auto: bool = True) -> None: self.running = False if restore_auto: try: self.set_auto_mode() print("Restored automatic fan control.", flush=True) except Exception as e: print(f"Failed to restore auto mode: {e}", file=sys.stderr, flush=True) def main() -> int: cfg = Config() ctl = AmdGpuFanController(cfg) def _handle_signal(signum, frame): ctl.stop(restore_auto=True) raise SystemExit(0) signal.signal(signal.SIGINT, _handle_signal) signal.signal(signal.SIGTERM, _handle_signal) try: ctl.run() return 0 except KeyboardInterrupt: ctl.stop(restore_auto=True) return 0 except Exception as e: print(f"Fatal error: {e}", file=sys.stderr, flush=True) ctl.stop(restore_auto=True) return 1 if __name__ == "__main__": sys.exit(main())

# Manual fan control enabled. temp= 64.0C rpm=2275 err=+34.0 dT/dt=+0.50C/s pwm=180 temp= 63.0C rpm=2448 err=+33.0 dT/dt=-0.50C/s pwm=198 temp= 63.0C rpm=2593 err=+33.0 dT/dt=+0.00C/s pwm=216 temp= 63.0C rpm=2735 err=+33.0 dT/dt=+0.00C/s pwm=234 temp= 63.0C rpm=2937 err=+33.0 dT/dt=+0.00C/s pwm=255 temp= 61.0C rpm=2937 err=+31.0 dT/dt=-1.00C/s pwm=255 #температура падает <-- вентиляторы растутСлева - обороты и момент включения скрипта. Справа - температура и ее падение ниже порога алерта

Слева - обороты и момент включения скрипта. Справа - температура и ее падение ниже порога алерта

Производительность

На длинной генерации удалось получить ~42 токен/сек для модели: Ministral 3B Q6_K

Финальная рабочая конфигурация

Host

  • OS: Ubuntu 22.04

  • kernel: 6.8.0-101-generic

  • GPU: AMD RX580 (gfx803)

Kubernetes (финальный ROCm профиль)

  • image: rocm/llama.cpp:llama.cpp-b6356_rocm6.4.3_ubuntu24.04_server

  • model: Ministral-3b-instruct.Q6_K.gguf

Ключевые env:

HSA_OVERRIDE_GFX_VERSION=8.0.3 HIP_VISIBLE_DEVICES=0 ROCR_VISIBLE_DEVICES=0 GPU_MAX_HW_QUEUES=1 LD_LIBRARY_PATH= #с путями ROCm runtime

Заключение

RX580 — не самая очевидная карта для LLM. Но наш эксперимент показывает:

Главное — понимать границы совместимости ROCm и внимательно диагностировать каждый слой системы.

Источник

Возможности рынка
Логотип NodeAI
NodeAI Курс (GPU)
$0.03112
$0.03112$0.03112
-5.43%
USD
График цены NodeAI (GPU) в реальном времени
Отказ от ответственности: Статьи, размещенные на этом веб-сайте, взяты из общедоступных источников и предоставляются исключительно в информационных целях. Они не обязательно отражают точку зрения MEXC. Все права принадлежат первоисточникам. Если вы считаете, что какой-либо контент нарушает права третьих лиц, пожалуйста, обратитесь по адресу [email protected] для его удаления. MEXC не дает никаких гарантий в отношении точности, полноты или своевременности контента и не несет ответственности за любые действия, предпринятые на основе предоставленной информации. Контент не является финансовой, юридической или иной профессиональной консультацией и не должен рассматриваться как рекомендация или одобрение со стороны MEXC.