Мы пытались запустить 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
GPU корректно определился:
library=rocmcompute=gfx803
Но ошибка hipMemGetInfo никуда не исчезла. Оно и понятно, поддержка этого семейства видеокарт прекратилась в ROCm 4.5
Первый шаг — убедиться, что контейнер видит 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.
При запуске модели:
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
После перезагрузки поведение изменилось радикально. Модель начала стабильно загружаться и выдавать токены.
После стабилизации 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, а не случайностью сэмплинга.
Для 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-регулятор оборотов
Ниже минимальный 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
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. Но наш эксперимент показывает:
Главное — понимать границы совместимости ROCm и внимательно диагностировать каждый слой системы.
Источник

