На узле моей AI‑платформы крутятся 34 контейнера: Dify, RAGFlow, векторные базы, мониторинг и SSO. Большой языковой модели среди них нет: основную генерацию стек получает по LAN с DGX Spark. На втором таком же мини‑ПК я отдельно поднял локальную Qwen3.6–35B‑A3B и прогнал серию замеров от 1K до 64K при контекстном окне 256K.

Обе машины — Beelink GTR9 Pro на Ryzen AI Max+ 395 (Strix Halo). Ниже — что эти коробки реально умеют: 117,4 ГиБ GTT после настройки ttm.pages_limit, p50/p95 локальных эмбеддингов и реранка, около 70 tok/s генерации через Vulkan/RADV и три грабли gfx1151.


Что я хотел проверить

  • Держит ли машина полный Docker‑стек приватной AI‑платформы в непрерывной работе.

  • Есть ли linux/amd64‑образы для всего каталога, или придётся собирать своё.

  • Работает ли Radeon 8060S (gfx1151) через Vulkan как основной путь инференса.

  • Сколько из 128 ГБ общей памяти реально достаётся GPU и какой настройкой.

  • Пригодны ли локальные эмбеддинги и реранкер для рабочего RAG‑пайплайна.

  • Какие ограничения у ROCm и мониторинга на этой платформе.

Стенд и рабочая связка версий

Оба узла одинаковы по железу и системному ПО:

Параметр

Значение

Машина

Beelink GTR9 Pro

APU

Ryzen AI Max+ 395 (Strix Halo)

CPU

16C/32T Zen 5

GPU

Radeon 8060S, 40 CU RDNA 3.5, gfx1151

Память

128 ГБ LPDDR5X (единый пул)

Накопитель

NVMe Crucial P310 2 ТБ

ОС

Ubuntu 24.04.4 LTS

Ядро

6.17.0–35-generic (HWE)

Mesa / RADV

25.2.8

ROCm

7.2.3 (установленная и проверенная версия стенда)

BIOS

GTRPR05; выделенная в BIOS память GPU (UMA) — 512 МиБ

linux‑firmware

20240318.git3b128b60-0ubuntu2.27 (пакет Ubuntu 24.04)

GTT

ttm.pages_limit=30788203 страниц: 117,4 ГиБ системной памяти для GPU; применяется после перезагрузки

Режим питания

BIOS‑профиль не фиксировался; производитель заявляет режимы 120/140 Вт

Роли узлов разделены, поэтому измерения в статье не смешиваются:

Узел

Роль

Что измерялось

Strix‑A

сервисный слой, 34 контейнера

снимок ресурсов, лимиты, GTT‑настройка, embed/rerank, история рестартов

Strix‑B

локальная Qwen3.6–35B‑A3B

prompt processing, decode, память модели, температура и мощность

Во время замеров на Strix‑B в GTT постоянно находились два llama‑сервера стороннего проекта: модели 8B Q5_K_M и 4B Q8_0. Вместе с прочими буферами они занимали 11,8 ГиБ GTT. Запросов к ним не было, а загрузка GPU в паузах составляла 0%.

Что реально запустилось: 34 контейнера (Strix‑A)

В выбранной конфигурации работают 34 контейнера:

  • Dify: api, worker, web, plugin‑daemon и sandbox; отдельно Docling;

  • RAGFlow с MySQL, Elasticsearch и MinIO;

  • Milvus с etcd и отдельным MinIO, а также PostgreSQL и Redis;

  • llama‑embed и llama‑rerank;

  • Prometheus, Grafana, Loki, Alloy, cAdvisor, Alertmanager и три экспортёра;

  • Authelia, Traefik, n8n, Phoenix, Open WebUI, Portainer, SSRF‑прокси и Docker socket proxy.

Векторное хранилище этой конфигурации: Milvus. Qdrant и Weaviate есть в каталоге, но на этом узле не выбраны.

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

Radeon 8060S обслуживает две GPU‑задачи через llama.cpp/Vulkan: bge-m3-Q8_0 для эмбеддингов и bge-reranker-v2-m3-Q8_0 для реранка. Остальные сервисы работают на CPU. Генерация приходит со Spark через OpenAI‑совместимый API по локальной сети.

Стек развёрнут 7 июня. Хост перезагружался 25 июня, после чего контейнеры автоматически поднял systemd‑юнит. 30 июня Dify пересоздавался при плановом обновлении. 5 июля перезапускался демон Docker; благодаря live‑restore контейнеры продолжили работу.

На 10 июля аптайм хоста составлял 15 дней, а большинство контейнеров работали с 25 июня. У текущих экземпляров 33 контейнеров из 34 RestartCount=0; у dify‑plugin‑daemon значение равно 5. Это не полная история месяца, потому что пересоздание Dify 30 июня завело новые экземпляры и обнулило их счётчики.

После перезапуска демона с live‑restore команда docker ps показывала Up 46 hours, хотя контейнеры фактически стартовали 25 июня. В этом случае время запуска я сверял через docker inspect … State.StartedAt; колонка STATUS вводила в заблуждение.

Снимок ресурсов сделан 10 июля одной командой docker stats --no-stream; проценты просуммированы. Память в docker stats представляет working set, который показывает CLI, а не сумму RSS. Поэтому её нельзя складывать с MemAvailable.

  • Контейнеры стека заняли около 21 ГиБ памяти.

  • Суммарный CPU составил около 4% по шкале Docker, где 100% соответствует одному логическому CPU. В этом снимке это примерно 0,04 полностью занятого ядра.

  • Среднее значение load average за минуту: 1,49 при 32 логических CPU. Это число runnable‑ и uninterruptible‑задач, а не процент загрузки. Длительный замер через mpstat я не проводил.

  • Ядро оценивало 98,8 из 124,9 ГиБ как доступные без активного ухода в swap.

  • В простое GPU держался на 43°C, gpu_busy_percent был равен 0.

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

Запас по лимитам под нагрузкой

У каждого сервиса в каталоге прописан mem_limit. Я прогнал 90 секунд конкурентной нагрузки (4 параллельных потока эмбеддингов батчами по 32 плюс 2 потока реранка) и снял docker stats в середине:

Контейнер

Занято / лимит

 лимита

dify‑sandbox

973 МиБ / 1 ГиБ

95%

alloy

341 МиБ / 512 МиБ

67%

dify‑plugin‑daemon

1,27 ГиБ / 2 ГиБ

64%

elasticsearch

4,7 ГиБ / 8 ГиБ

58,5%

openwebui

1,12 ГиБ / 2 ГиБ

56%

llama‑embed (под нагрузкой)

1,4 ГиБ / 8 ГиБ

18%

llama‑rerank (под нагрузкой)

0,8 ГиБ / 8 ГиБ

10%

остальные

< 55%

Ближе всех к своему потолку оказался dify‑sandbox: 95% в одном снимке. Нагрузка шла на embed/rerank и не затрагивала sandbox. Поэтому сначала нужно проверить его memory.currentmemory.peak и memory.events, а затем прогнать нагрузку с выполнением кода. Одного снимка недостаточно, чтобы автоматически повышать лимит.

Низкие значения docker stats у GPU‑сервисов не означают малый расход GPU‑памяти. Под нагрузкой Radeon была загружена на 100%, температура достигала 60°C, а power1_average показывал около 120 Вт на уровне APU/socket. Docker CLI вычитает inactive_file из отображаемого значения, а отдельный объём занят в GTT. Сопоставление cgroup и GTT приведено в разделе про генерацию.

Как Strix Halo отдаёт 128 ГБ: TTM, а не только BIOS

Ключевой момент, без которого стенд не воспроизвести. Снимок с работающего Strix‑A:

MemTotal:     131014476 kB   # 124.9 GiB
MemAvailable: 103555740 kB   #  98.8 GiB при 34 работающих контейнерах
mem_info_vram_total:  0.5 GiB   # выделено в BIOS (UMA), минимально
mem_info_gtt_total: 117.4 GiB   # GTT-пул, из которого GPU берёт память

117,4 ГиБ не являются значением по умолчанию. GTT (Graphics Translation Table) — пул системной памяти, которую драйвер отображает для GPU; управляет им TTM, подсистема памяти GPU в ядре Linux. Без настройки размер GTT на этой платформе обычно составляет примерно половину системной памяти. Параметр ttm.pages_limit=30788203 задаёт максимальное число системных страниц, которое TTM может отобразить для GPU. Наша Ansible‑роль установки вычисляет его как 94% RAM; после перезагрузки это значение появляется в mem_info_gtt_total.

AMD рекомендует для большого GPU‑пула оставить минимальную выделенную в BIOS память и увеличить pages_limit. Фактическая командная строка ядра:

$ cat /proc/cmdline
... amd_iommu=off ttm.pages_limit=30788203 ttm.page_pool_size=30788203 zswap.enabled=0 ...

ttm.page_pool_size управляет кэширующим пулом WC/UC/DMA‑страниц TTM и не задаёт размер GTT. В нашей роли он выставлен тем же числом, но это отдельная настройка. То же относится к zswap.enabled=0 и amd_iommu=off.

Отключение IOMMU влияет на DMA‑изоляцию и виртуализацию, поэтому этот параметр нельзя без проверки переносить на хост с виртуальными машинами или PCI passthrough. Отдельный эффект от него на производительность этого стенда не измерялся. Для большого GTT из перечисленного обязателен только pages_limit; остальные параметры не образуют универсального пресета.

CPU‑контейнеры и GPU‑нагрузка используют один физический пул и конкурируют как за объём, так и за пропускную способность памяти. MemAvailable оценивает, сколько памяти система может выделить без активного ухода в swap; это не гарантированная квота для следующего выделения памяти GPU. На момент снимка у полного сервисного стека оставался большой запас общей памяти.

Как завёлся GPU: RADV основной, ROCm запасной

На Spark мы были привязаны к конкретной ветке проприетарного драйвера; эту историю я разбирал отдельно. На Strix Halo вопрос стоял иначе: какой GPU‑путь достаточно стабилен для постоянного сервиса.

Для наших узлов основным путём стал Vulkan через RADV. Открытый Mesa‑драйвер обновляется штатным apt upgrade:

Available: ['vulkan', 'rocm', 'cpu']
Selected:  vulkan / llama_cpp
Device:    AMD Radeon 8060S (Strix Halo, gfx1151)
Memory:    117.4 GiB
  driver_id: DRIVER_ID_MESA_RADV     mesa_version: 25.2.8
  cooperative_matrix: True           is_strix_halo: True
  engine_implementation: llama-cpp-python (GGML_VULKAN)

cooperative_matrix: True означает, что устройство и сборка позволяют llama.cpp использовать шейдеры cooperative matrix. Разницу с отключённым путём я не измерял, поэтому эта строка подтверждает наличие возможности, а не конкретный прирост. За время работы узлов фиксировать Mesa на одной версии не пришлось.

ROCm установлен как запасной путь. AMD впервые включила gfx1151 и Ryzen AI Max+ 395 в матрицу поддержки ROCm для Ryzen начиная с 6.4.4. Текущая официальная матрица документирует ROCm 7.2.1 на Ubuntu 24.04 с ядрами, содержащими необходимые KFD‑исправления.

На наших узлах установлен более поздний пакет 7.2.3 из официального репозитория, и эта конфигурация проверена локально. gfx1151 определяется штатно, поэтому HSA_OVERRIDE_GFX_VERSION не используется. Это результат конкретного стенда, а не обещание для любого дистрибутива.

Три грабли gfx1151

1. ROCm требует точного сочетания версий. На наших узлах проверена следующая комбинация: Ubuntu 24.04.4, HWE‑ядро 6.17.0–35, ROCm 7.2.3 и пакет linux-firmware 20240318.git3b128b60-0ubuntu2.27.

Считать её универсальным минимумом нельзя. AMD отдельно оговаривает версии ядра для gfx1151, а отчёты сообщества связывают некоторые снимки firmware из upstream с регрессиями на Strix Halo, в частности 20251125. Номер Ubuntu‑пакета с базой 20 240 318 и дистрибутивными backports нельзя напрямую сравнивать с датой upstream‑снимка. Для воспроизведения важна полная версия пакета.

Для ROCm 7.1 публично описан GPU hang при одновременной AI‑нагрузке и аппаратном кодировании видео (ROCm/ROCm#5665). Issue закрыт, но публичное обсуждение не позволяет связать исправление именно с ROCm 7.2.3. Этот случай показывает, насколько чувствительна вся связка ядра, firmware и userspace.

2. amd-smi не видит метрики, которые доступны в sysfs. На gfx1151 amd-smi возвращает N/A по ключевым показателям; проблема описана в открытом ROCm/ROCm#6035. Официальный AMD Device Metrics Exporter рассчитан на MI2xx/MI3xx, поэтому на эти узлы мы его не ставим.

Температура, мощность и загрузка доступны через sysfs/hwmon. Номер hwmonN меняется между загрузками, поэтому рабочий скрипт перебирает card* и выбирает устройство с vendor ID AMD 0x1002. На этом хосте Radeon соответствует card0:

/sys/class/drm/card0/device/hwmon/hwmon*/temp1_input      → 50000   (50°C)
/sys/class/drm/card0/device/hwmon/hwmon*/power1_average   → ~40 Вт  # scope: APU/socket, не отдельный iGPU
/sys/class/drm/card0/device/gpu_busy_percent              → 0…100

Короткий скрипт преобразует эти значения в textfile‑формат для node‑exporter, после чего их забирает Prometheus. Решение проверено на нашей текущей связке amdgpu и sysfs и не зависит от полноты amd-smipower1_average не показывает изолированное потребление iGPU; потребление всей машины из розетки также не измерялось.

3. Неверное имя ICD‑манифеста отключает GPU без явной ошибки. В образе llama.cpp server‑vulkan RADV‑манифест называется radeon_icd.json. Привычного Debian‑multiarch имени radeon_icd.x86_64.json там нет. Если указать отсутствующий файл в VK_DRIVER_FILES, Vulkan loader не находит драйвер.

Эту проблему мы обнаружили на работающем узле. Строка loaded Vulkan backend подтверждает только загрузку библиотеки, поэтому проверка состоит из двух частей: llama.cpp должен перечислить устройство и сообщить о выгрузке слоёв на GPU (offload).

llama_prepare_model_devices: using device Vulkan0 (Radeon 8060S Graphics (RADV STRIX_HALO))
load_tensors: offloaded 25/25 layers to GPU

Сервис считается работающим на GPU только после обеих проверок. Опираться на одну фиксированную строку нельзя, потому что формат логов меняется между версиями llama.cpp.

Эмбеддинги и реранкер: p50/p95 (Strix‑A)

Образ llama.cpp: server-vulkan-b9049. Embed‑сервис запущен с --pooling cls--ctx-size 8192--parallel 4, KV f16 и -ngl 999.

Использовались две модели:

  • lm-kit/bge-m3-gguf, файл bge-m3-Q8_0.gguf, SHA-256 6fae7a1e5c8039c7aff595830fc7b5551b4426bc679f5e526f737e2f63811afa;

  • gpustack/bge-reranker-v2-m3-GGUF, файл bge-reranker-v2-m3-Q8_0.gguf, SHA-256 a43c7c9b11a4c1517e5bf95151960e1621d1b72f7a493364b01e386cf1aaa1d3.

Запросы выполнялись с localhost без reverse proxy. Для каждой точки использовался один и тот же русский текст примерно на 40 слов: 5 прогревочных и 50 измерительных повторов. В rerank‑тесте длина 1840 токенов взята из usage.prompt_tokens ответа сервера. Лог загрузки подтверждает offloaded 25/25 layers to GPU.

Задача

p50

p95

embed, 1 текст

11,9 мс

12,3 мс

embed, батч 32 текста

182 мс

184 мс

rerank, запрос + 20 документов (1840 токенов)

141 мс

142 мс

Температура составляла 43°C в простое и 53–60°C во время 75-секундного цикла embed+rerank при загрузке GPU 88–90%. Через 10 секунд после остановки теста она снизилась до 51°C. Это короткий тест, а не проверка многочасового теплового режима.

В нашем сценарии с небольшой параллельностью около 140 мс на реранк двадцати кандидатов не являются доминирующей задержкой. Поведение под параллельными клиентами и p99 не измерялись.

Qwen3.6–35B‑A3B: окно 256K, измерения до 64K (Strix‑B)

На втором узле установлена локальная LLM для автономной работы без Spark. Пока писалась статья, контейнер был остановлен ради другого проекта. Я снова запустил его и провёл отдельный тест.

Методика. Конфигурация Strix‑B приведена выше; режим питания BIOS не фиксировался.

Образ: ghcr.io/ggml-org/llama.cpp:server-vulkan-b9049@sha256:e359c01203861cf1d802795b8d443ed44660b6ff3f61ba04d7de6ead781c580a.

Модель: 0xSero/Qwen3.6-35B-GGUF, файл Qwen3.6-35B-A3B-Q4_K_M.gguf, около 20 ГБ, SHA-256 1d8eda2b6616d1b163d3b8cf9e0ba2cb627b98fe8553190e3183e1553ec82130. Автор GGUF‑сборки публикует для этого файла 70,2 tok/s на том же классе железа: Ryzen AI Max+ 395, Vulkan, KV q8_0, batch/ubatch 2048. Это независимая проверка порядка величины.

Сервер запущен с -ngl 999--flash-attn on, KV q8_0 для K и V, batch/ubatch 2048, --no-mmap--ctx-size 262144 и --parallel 1. Vision projector и MTP (multi‑token prediction) не использовались.

Запросы отправлялись в raw POST /completion без шаблона чата (chat template). Поэтому это тест режима completion, а не штатного чат‑режима Qwen с рассуждениями (thinking). Переключатель enable_thinking не применялся. В некоторых длинных ответах при t=0.7 модель генерировала <think>‑блоки; для измерения это обычные выходные токены, отдельно их не вычитали.

Параметры контекстного теста: n_predict=128temperature=0cache_prompt=false. Seed не задавался.

Промпты формировались через /tokenize и /detokenize. В таблице указана фактическая длина из ответа сервера (timings.prompt_n). Для каждой точки выполнено по два прогона; значения различались не более чем на 2%, поэтому в таблице приведено среднее. Для 64K был один прогон без оценки разброса.

ignore_eos не задавался, а tokens_predicted и stop_type не сохранялись. Поэтому значения decode в контекстном тесте следует считать ориентировочными. Порядок величины дополнительно подтверждают длинные прогоны на 1024 и 2048 выходных токенов.

PP в таблице означает prompt processing, то есть обработку промпта; столбец «Prompt processing, с» содержит серверное prompt_ms, а не клиентский TTFT (time to first token — время до первого токена). Decode — этап генерации выходных токенов.

Промпт, токенов

Prompt processing, с

PP, tok/s

Decode на этой глубине, tok/s

1 006

0,91

1 100

70,5

4 078

4,4

935

69,2

8 174

9,0

905

67,8

16 366

19,4

843

65,5

32 749

44,5

735

61,4

65 518

116,9

561

54,2

TTFT в стриминге измерялся отдельно: 1,01 с при 1K и 8,96 с при 8K. Разница с prompt_ms составила около 0,1 с в обе стороны, но этот тест не разделяет сетевые, клиентские и серверные накладные расходы.

Рабочие сценарии в дополнение к замерам по глубине:

Сценарий

Результат

Генерация кода: потокобезопасный LRU‑кэш с TTL, типизацией и pytest (1024 токена кода, промпт 71 токен)

70,3 tok/s; модель сгенерировала типизированную реализацию с RLock и OrderedDict плюс pytest‑тесты; сами тесты отдельно не запускались

Длинная генерация: разбор 8K‑промпта, 2048 токенов, t=0.7

средняя скорость за 2048 выходных токенов: 67,1 tok/s; остановок, ошибок и видимых замираний не было

Загрузка GPU при decode; power1_average (APU/socket)

96–97%; 52–53°C; 77–85 Вт

Загрузка GPU при PP на 64K; power1_average (APU/socket)

100%; 62–67°C; 101–104 Вт

Память модели: cgroup, Docker CLI и GTT. У экземпляра Qwen, на котором выполнялись тесты, memory.current составлял 18,63 ГиБ, а memory.peak достигал 19,77 ГиБ. В то же время docker stats показывал 1,5 ГиБ, а общий mem_info_gtt_used узла составлял 39,5 ГиБ.

После docker stop значение GTT стабилизировалось на 11,8 ГиБ, которые занимали фоновые задачи соседнего проекта. Разница составила 27,7 ГиБ GTT.

В логах llama.cpp указано около 25,4 ГиБ буферов: 19,4 ГиБ весов (19 902,9 MiB), 2,7 ГиБ KV и примерно 3,3 ГиБ рабочих буферов. Оставшиеся ~2,3 ГиБ в этом логе не расшифрованы. Они могут относиться к дополнительным выделениям памяти бэкенда или драйвера либо к разнице во времени измерений.

Контрольный замер после перезапуска контейнера показал другую картину. Модель снова находилась в GTT, но memory.current составлял всего 0,59 ГиБ: anon 0,57 ГиБ и inactive_file 0,01 ГиБ. В этом запуске GTT‑страницы модели почти не учитывались в memory cgroup (memcg) контейнера. Значительная часть 18,6 ГиБ прежнего экземпляра, вероятно, приходилась на страничный кэш после чтения 20-гигабайтного файла модели. Именно его (inactive_filedocker stats вычитает из отображаемого значения.

memory.current и mem_info_gtt_used описывают разные уровни учёта. На этом стенде нужно наблюдать оба показателя вместе с memory.events, а GPU‑память планировать по mem_info_gtt_used. Этот вывод относится к конкретной связке ядра, amdgpu и бэкенда.

GTT почти не менялся при заполнении контекста, потому что сервер был запущен сразу с окном 256K и выделил максимальные буферы при старте.

Почему KV занимает только 2,7 ГиБ при окне 256K. В карточке модели указано 40 слоёв, организованных в десять групп, в каждой — три слоя Gated DeltaNet и один обычный attention. Классический KV‑кэш нужен только десяти attention‑слоям, у каждого по две KV‑головы размерностью 256.

С учётом служебных данных q8_0 занимает примерно 1,0625 байта на элемент: 34 байта на блок из 32 значений. Расчёт 262 144 × 10 × 2 × 256 × 2 × 1,0625 даёт около 2,66 ГиБ — ровно те 2 720 MiB, что сервер сообщил в логе.

В карточке модели указано 40 transformer‑слоёв. Строка offloaded 41/41 layers to GPU отражает внутренний счётчик объектов offload в этой сборке llama.cpp, а не другую спецификацию модели.

Интерпретация. Скорость decode уменьшается примерно на 13% к 32K и на 23% к 64K. Для интерактивной работы сильнее влияет обработка промпта: её время выросло с 0,9 до 116,9 секунды. Сценарий «загрузить книгу и сразу задать вопрос» без кэша префикса становится некомфортным.

При сохранённом кэше префикса повторные запросы с тем же началом могут не пересчитывать весь документ. Такой кэш не постоянен: он вытесняется и теряется при перезапуске. Глубину больше 64K я не измерял. По экстраполяции текущей кривой один прогон на 128K занял бы несколько минут только на обработку промпта.

Около 70 tok/s на модели с 35 млрд параметров объясняются MoE: на каждом токене активны примерно 3 млрд параметров, поэтому объём чтения весов заметно меньше полного размера модели. Для плотной модели с весами около 20 ГБ идеализированная оценка по пропускной способности 256 ГБ/с даёт примерно 12 tok/s. Это верхняя граница, а не результат измерения.

Аудит образов: коротко

В каталоге платформы 47 сервис‑дескрипторов. Три образа собираются локально, 44 загружаются из registry. После дедупликации общих образов осталось 40 уникальных внешних пинов. Аудит проводился по digest, которые фактически использует рантайм, а не по изменяемым тегам. Результат на 10 июля 2026 года, commit e04aad6:

  • linux/amd64: 40 из 40 пинов; специальные ARM‑сборки инфраструктурных сервисов для этих узлов не нужны;

  • linux/arm64: 37 из 40. Поддержки arm64 нет у upstream‑образа RAGFlow, нашего hotfix‑пина MinIO и пина Squid, зафиксированного на одну архитектуру; upstream‑тег Squid при этом мультиархитектурный;

  • у трёх тегов текущий digest не совпал с пином. Это pgvector:pg17squid:7.2-26.04_edge и elasticsearch:8.19.16. Для Squid с пином сравнивался amd64-манифест из текущего OCI‑индекса — объекты одного уровня. Про Elasticsearch можно утверждать только, что тег сейчас ведёт не на наш digest. Без исходной записи на момент пиннинга нельзя доказать, что semver‑тег был передвинут.

На развёртывание эти расхождения не повлияли, потому что рантайм использует digest.

Все 40 пинов лежат в дескрипторах открытого репозитория; каждый проверяется одной командой docker buildx imagetools inspect <repo>@sha256:<digest>.

Что осталось на Spark и как машины связаны

Основная модель работает на паре DGX Spark; связку по 200G я разбирал в статье про multi‑user. Сервисный стек обращается к ней через OpenAI‑совместимый API по локальной сети.

В обычной Compose bridge‑сети контейнеры находят друг друга по имени сервиса. Например, http://llama-llm:8080/v1 использует Docker DNS. Между независимыми Compose‑хостами такое имя не резолвится, поэтому нужен опубликованный LAN‑адрес или FQDN. Исключение составляет специально настроенная multi‑host overlay‑сеть, которой у нас нет.

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

Единой плоскости управления (control plane) у четырёх машин пока нет. Межхостовая оркестрация, переключение при отказе (failover) и откат всего кластера остаются экспериментальными. Сейчас это узлы с фиксированными ролями и LAN‑маршрутизацией.

Стек разворачивает переписанная платформа на валидируемых YAML‑дескрипторах с digest‑пинами — на смену прежнему Bash‑инсталлятору. Её архитектура и миграционные дефекты будут темой отдельной статьи.

Сравнение с DGX Spark: коротко, для контекста

DGX Spark (GB10)

Beelink GTR9 Pro (Ryzen AI Max+ 395)

ISA

AArch64

x86-64

GPU‑стек

CUDA (проприетарный драйвер)

Vulkan/RADV (Mesa) + ROCm вторично

Память

128 ГБ unified, 273 ГБ/с

128 ГБ unified, ~256 ГБ/с (теор., 256-бит LPDDR5X-8000)

Роль у нас

генерация одной большой моделью

универсальный сервисный узел

Цены на 10 июля 2026 года: Spark с 128 ГБ / 4 ТБ стоил $4 699; GTR9 Pro с 128 ГБ / 2 ТБ предлагался за $4 349 по pre‑sale при зачёркнутой цене $4 699. Мощность блоков питания составляет 240 и 230 Вт. Потребление из розетки не измерялось, а прямых бенчмарков между машинами в статье нет: они выполняют разные роли.

Результат, ограничения и что не проверено

Работает. На Strix‑A запущены 34 контейнера выбранной конфигурации. Для всех 40 внешних пинов существует linux/amd64. Эмбеддинги и реранк выполняются на Radeon 8060S через RADV; p95 составил 12,3 мс для embed и 142 мс для rerank. На Strix‑B Qwen3.6–35B‑A3B показала 70,5 tok/s на коротком контексте и 54,2 tok/s на глубине 64K при окне 256K. В тесте генерации кода скорость была около 70 tok/s.

При работающем стеке ядро оценивало 98,8 ГиБ памяти как доступные. На момент снимка у 33 из 34 текущих экземпляров RestartCount был равен нулю; полная история месяца включает плановое пересоздание Dify 30 июня. Стек пережил перезагрузку хоста с автозапуском контейнеров через systemd‑юнит и перезапуск демона Docker с live‑restore. Аптайм хоста составлял 15 дней.

Ограничения. Мониторинг gfx1151 остаётся незрелым: amd-smi не отдаёт нужные метрики, поэтому используются данные sysfs. ROCm чувствителен к сочетанию ядра, firmware и userspace и остаётся запасным путём. Большой GTT требует настройки ttm.pages_limit и перезагрузки.

В одном снимке dify‑sandbox достиг 95% лимита; ему нужен отдельный тест с выполнением кода. dify‑plugin‑daemon имеет пять рестартов у текущего экземпляра. GPU‑память планируется по mem_info_gtt_used, а не по docker stats.

Не проверено. Потребление из розетки; фиксация режима питания BIOS; время полного подъёма стека с нуля на этом железе; глубина контекста за 64K и параллельные потоки генерации (--parallel 1); embed/rerank под конкурентными клиентами (p99); multi‑node‑режим как единый кластер; многочасовые тепловые режимы; сравнение производительности Vulkan и ROCm на этих задачах.


Strix Halo не заменил Spark как ускоритель основной LLM. Две одинаковые машины закрыли другие роли: плотный сервисный стек и локальный Vulkan‑инференс MoE‑модели. Основные ограничения связаны с мониторингом gfx1151 и чувствительностью ROCm к версиям ядра и firmware.

Для сервисного узла важнее оказались 128 ГБ общей памяти и обычная x86-64-экосистема, а не максимальная скорость GPU.

Код платформы открыт под Apache 2.0: github.com/botAGI/AGmind64 (упоминаемый commit e04aad6 — оттуда). Вопросы и найденные грабли обсудим в комментариях.