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м. чтение
Для обратной связи или замечаний по поводу данного контента, свяжитесь с нами по адресу crypto.news@mexc.com
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,0292
$0,0292$0,0292
-2,69%
USD
График цены NodeAI (GPU) в реальном времени
Отказ от ответственности: Статьи, размещенные на этом веб-сайте, взяты из общедоступных источников и предоставляются исключительно в информационных целях. Они не обязательно отражают точку зрения MEXC. Все права принадлежат первоисточникам. Если вы считаете, что какой-либо контент нарушает права третьих лиц, пожалуйста, обратитесь по адресу crypto.news@mexc.com для его удаления. MEXC не дает никаких гарантий в отношении точности, полноты или своевременности контента и не несет ответственности за любые действия, предпринятые на основе предоставленной информации. Контент не является финансовой, юридической или иной профессиональной консультацией и не должен рассматриваться как рекомендация или одобрение со стороны MEXC.

Вам также может быть интересно

Playnance связывает активность пользователей с ростом сети через новый протокол социального гейминга

Playnance связывает активность пользователей с ростом сети через новый протокол социального гейминга

Статья о том, как Playnance связывает активность пользователей с ростом сети с помощью нового протокола социальных игр, появилась на BitcoinEthereumNews.com. Playnance представляет новый
Поделиться
BitcoinEthereumNews2026/03/24 04:46
Революционная структура конфиденциальности Solana Foundation ускоряет корпоративное внедрение криптовалют

Революционная структура конфиденциальности Solana Foundation ускоряет корпоративное внедрение криптовалют

BitcoinWorld Революционная система конфиденциальности Solana Foundation ускоряет корпоративное внедрение криптовалют Solana Foundation представила инновационный подход
Поделиться
bitcoinworld2026/03/24 04:55
Новая криптовалюта: прогноз цены Shiba Inu останавливается, пока JPMorgan продвигает токенизированные доллары

Новая криптовалюта: прогноз цены Shiba Inu останавливается, пока JPMorgan продвигает токенизированные доллары

Традиционные финансы теперь углубляются в блокчейн, при этом банки фокусируются на регулируемых рельсах. Розничные трейдеры, с другой стороны, следят за мем-коинами
Поделиться
Techbullion2026/03/24 05:36