- BrainTools - https://www.braintools.ru -
Мы пытались запустить 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, температура, частоты);
Мы предположили, что проблема может быть в userspace‑части ROCm. Попробовали альтернативный вариант – взять более “готовый” образ из гитхаба woodrex83/ROCm-For-RX580 [1]
GPU корректно определился:
library=rocmcompute=gfx803
Но ошибка [2] hipMemGetInfo никуда не исчезла. Оно и понятно, поддержка этого семейства видеокарт прекратилась в ROCm 4.5
Первый шаг — убедиться, что контейнер видит GPU. В Kubernetes доступ к девайсам обеспечил AMD GPU Operator [3], в докере для дебага нужно смонтировать /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.
При запуске модели:
ollama run tinylama
Появлялась ошибка CUDA error: invalid argument hipMemGetInfo(free, total)
Интересно, что при этом:
pod был healthy
API отвечал
VRAM резервировалась
На первый взгляд система выглядела рабочей. Но inference либо падал, либо выдавал мусор. Это уже четко указывало на runtime-цепочку
kernel -> ROCm runtime -> ggml backend.
Следующим подозреваемым стал runtime внутри inference backend.
Ollama использует ggml, который взаимодействует с ROCm через HIP. Но на этом этапе было непонятно — проблема в runtime или в устаревшем железе
Чтобы проверить гипотезу, мы попробовали альтернативный 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 заработал с первого запуска. Это означало:
GPU исправен
Модель работает
gglm не причем
Мы проверили еще несколько вариантов userspace:
GPU по-прежнему детектился;
класс ошибок hipMemGetInfo не исчезал полностью;
часть симптомов менялас��, но root cause не уходил.
Хост работал на: kernel 5.15.0-171
Симптомы:
ROCm видел GPU
runtime иногда падал при старте
Мы попробовали более новое ядро: 6.8.0–101 как проверка одной из гипотез совместимости версий kernel ↔ ROCm userspace ↔ ggml
После перезагрузки поведение [4] изменилось радикально. Модель начала стабильно загружаться и выдавать токены.
После стабилизации 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 жёстко ограничивает декодер и «прячет» часть поведенческих проблем, поэтому мы использовали его как контроль, а затем вернулись к 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, а не случайностью [5] сэмплинга.
Для API-сценариев со строгим парсингом используемjson schema (где это поддерживается).
Итого – строгие форматы решаются на уровне контракта ответа, а не форсированием каждого токена грамматикой.
Проблема была не только в inference, но и в наблюдаемости.
Мы одновременно работали с тремя источниками:
default-metrics-exporter (от AMD GPU Operator – изначально показался наиболее логичным и prod-ready)
radeon-exporter ( kmulvey/radeon_exporter:latest – в итоге именно он неизменно отдавал хоть и мало, но точных метрик)
Грубый fallback на прямое чтение sysfs (/sys/class/drm/card*/device/gpu_busy_percent)
Также выяснилось что стандартный драйвер видеокарты поддерживает управление вентиляторами только auto/manual
На auto температура улетала в небеса
На manual только фиксированное значение оборотов
-> Пользуемся знаниями теории управления и пишем простой PID-регулятор оборотов
#!/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
OS: Ubuntu 22.04
kernel: 6.8.0-101-generic
GPU: AMD RX580 (gfx803)
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. Но наш эксперимент показывает:
Даже старая RX580 способна запускать современные LLM. И даже в k8s окружении.
Главное — понимать границы совместимости ROCm и внимательно диагностировать каждый слой системы.
Автор: alexkekiy
Источник [6]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/27134
URLs in this post:
[1] woodrex83/ROCm-For-RX580: https://github.com/woodrex83/ROCm-For-RX580?ysclid=mmkg5j2vfa566310755
[2] ошибка: http://www.braintools.ru/article/4192
[3] AMD GPU Operator: https://rocm.github.io/gpu-operator
[4] поведение: http://www.braintools.ru/article/9372
[5] случайностью: http://www.braintools.ru/article/6560
[6] Источник: https://habr.com/ru/articles/1010358/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1010358
Нажмите здесь для печати.