Files
CFDManager/docs/theory/2d_solver/bench/README.md
T
NotBigGhostandClaude Opus 5 81985b169e Запуск в WSL2 через docker compose без NVIDIA-runtime
В WSL2 драйвера Vulkan для Linux у NVIDIA нет: карта отдаётся через /dev/dxg по
протоколу WDDM, нативный libGLX_nvidia про него не знает и перечисляет ноль
устройств, поэтому Container Toolkit нечего подкладывать внутрь и docker падает
с `could not select device driver "nvidia"`.

С /dev/dxg умеет говорить dzn (Dozen) — драйвер Mesa, транслирующий Vulkan в
D3D12. Он положен в образ, и NVIDIA-runtime для этого пути не нужен вовсе: нужны
проброс устройства и монтирование /usr/lib/wsl, где Microsoft держит libd3d12.so.

Правка в решателе одна: флаги инстанса wgpu теперь читаются из окружения
(InstanceFlags::from_build_config().with_env()). Без этого переменная
WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER не действует, а wgpu по умолчанию
МОЛЧА прячет адаптеры, не прошедшие тесты соответствия Vulkan, — под это правило
попадает dzn, и решатель сообщал, что GPU не найден. Поведение по умолчанию не
изменилось: без переменной такие адаптеры по-прежнему скрыты.

База образа сменена с debian:bookworm-slim на archlinux:base: в пакетах Mesa у
Debian и Ubuntu dzn не собирают (проверено по спискам файлов), в Arch он лежит
отдельным пакетом той же версии Mesa, что и на хосте WSL.

Новое:
 * docker-compose.wsl.yml — путь через /dev/dxg, с профилем проверок и с build:
   на случай, когда доступа к реестру нет;
 * bench/parity.py — сверка GPU-пути с CPU в f64 на течениях, где расхождение
   f32 и f64 не нарастает. Соответствие dzn вендором не проверено, значит
   проверяем сами, а не верим на слово.

Замерено на Intel Iris Xe (та же карта, нативный драйвер против dzn):
 * точность: cd 2.39486 против 2.39486, energy_end 5.74813e-05 против
   5.74810e-05 — совпадение до 5-6 значащих цифр;
 * скорость: плата за трансляцию падает с ростом сетки, 4.2x на 61 тыс. узлов,
   1.46x на 461 тыс., 1.30x на 1.84 млн. На 95.1% стоимости кампании сетки
   крупнее 600 тыс. узлов, поэтому ожидаемое удорожание — около трети, не в разы.

В калибровку добавлена сетка 1920x960: оценивать кампанию по 240x120 значит
занижать пропускную способность вчетверо. Образ опубликован как
notbigghost/kbc2d:1.1.0 (он же latest), проверен вытягиванием из реестра.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 17:15:53 +03:00

15 KiB
Raw Blame History

Валидационная кампания

115 прогонов, ≈90 часов на RTX 4070 Ti. Проверяет решатель по трём независимым линиям: эталонам из статей авторов метода, литературе по обтеканию тел и внутренним инвариантам самой схемы. Каждый прогон кладёт логи, ряды, машиночитаемую сводку и гифку в собственную папку.

Быстрый старт на сервере

Образ опубликован, собирать ничего не нужно: notbigghost/kbc2d:1.1.0. Исходники на сервере тоже не нужны — переносится один файл docker-compose.server.yml.

mkdir -p ~/kbc2d && cd ~/kbc2d          # сюда же ляжет ./out с результатами
# перенести сюда docker-compose.server.yml

C=docker-compose.server.yml
docker compose -f $C --profile check run --rm vulkan      # 1. карта видна из контейнера?
docker compose -f $C --profile check run --rm preflight   # 2. все 115 сценариев стартуют?
docker compose -f $C --profile check run --rm calibrate   # 3. сколько MLUPS на этой машине?
docker compose -f $C --profile check run --rm plan        # 4. смета в часах по замеренному
docker compose -f $C up -d                                # 5. кампания
docker compose -f $C logs -f

Порядок не случайный: узнать, что карта не видна, лучше на первом шаге, чем через час счёта.

Замеренные калибровкой числа подставляются переменными окружения — они влияют только на оценки в часах, не на счёт:

KBC2D_GPU_MLUPS=1450 KBC2D_CPU_MLUPS=40 docker compose -f $C --profile check run --rm plan

Если vulkaninfo показывает ноль адаптеров — почти наверняка дело в NVIDIA_DRIVER_CAPABILITIES: Container Toolkit подкладывает Vulkan-ICD только при наличии graphics в списке. В образе и в compose это прописано, но может быть переопределено снаружи. Проверить, что хост вообще умеет отдавать карту:

docker run --rm --gpus all nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi

Запуск в WSL2

Отдельный путь, потому что в WSL2 драйвера Vulkan для Linux у NVIDIA не существует. Карта отдаётся через /dev/dxg по протоколу WDDM; нативный libGLX_nvidia про него не знает и возвращает ноль устройств — загрузчик его выбрасывает. Отсюда и could not select device driver "nvidia": NVIDIA Container Toolkit тут не поможет, потому что подкладывать внутрь нечего.

Зато с /dev/dxg умеет говорить dzn (Dozen) — драйвер Mesa, транслирующий Vulkan в D3D12. Он лежит в образе. NVIDIA-runtime при этом не нужен вовсе: достаточно проброса устройства и монтирования /usr/lib/wsl, где Microsoft держит libd3d12.so.

C=docker-compose.wsl.yml
docker compose -f $C --profile check run --rm vulkan      # 1. карта видна?
docker compose -f $C --profile check run --rm parity      # 2. считает ли она правильно?
docker compose -f $C --profile check run --rm calibrate   # 3. и с какой скоростью?
docker compose -f $C --profile check run --rm preflight   # 4. все сценарии стартуют?
docker compose -f $C --profile check run --rm plan        # 5. смета в часах
docker compose -f $C up -d                                # 6. кампания

Если образа нет ни локально, ни в реестре — docker compose -f $C build, доступ к Docker Hub не обязателен.

Шаг parity обязателен

dzn сообщает о себе conformanceVersion = 0.0.0.0: набор тестов соответствия Vulkan он не проходил. wgpu по этой причине по умолчанию прячет такие адаптеры, и решатель сообщает, что GPU не найден. Согласие считать на непроверенном драйвере даётся явно — переменной WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER=1, она прописана в docker-compose.wsl.yml.

Раз соответствие не проверено вендором, его проверяем сами. bench/parity.py гоняет два коротких эталона на CPU в f64 и на GPU и сверяет числа: вихрь Тейлора–Грина (есть точное решение) и стационарное обтекание цилиндра при Re=20. Течения выбраны намеренно не хаотические — там расхождение f32 и f64 не нарастает, поэтому заметное различие означает проблему драйвера, а не разрядности.

Замерено на Intel Iris Xe: dzn совпадает с нативным драйвером той же карты до 5–6 значащих цифр (energy_end 3.02277e-07 против 3.02283e-07, cd 2.39486 против 2.39486). Точность трансляция не портит. Но это замер на Intel; на своей карте прогоните сами — две минуты.

Чего трансляция стоит по скорости

Плата есть, но она почти вся — накладные расходы на вызов, а не на счёт, и потому падает с ростом сетки. Замерено на Intel Iris Xe, один и тот же решатель:

сетка узлов нативный Vulkan dzn в контейнере плата
320×192 61 тыс. 188.8 MLUPS 44.5 MLUPS 4.2×
960×480 461 тыс. 125.3 MLUPS 86.0 MLUPS 1.46×
1920×960 1.84 млн 128.7 MLUPS 98.7 MLUPS 1.30×

Каждый шаг решателя — несколько отправок в очередь; на мелкой сетке трансляция вызова стоит дороже самого счёта, на крупной размазывается. Для кампании это решающее обстоятельство, потому что дорогие прогоны в ней как раз крупные:

узлов в сетке прогонов доля стоимости кампании
< 150 тыс. 18 0.0%
150–600 тыс. 47 4.9%
> 600 тыс. 50 95.1%

То есть 95% времени кампания проводит там, где плата 1.3–1.5×, и почти не бывает там, где она четырёхкратна. Ожидаемое удорожание по всей кампании — около трети: 90 часов превращаются примерно в 115–120, а не в 400.

Проверьте на своей карте: профиль calibrate меряет три сетки, и ориентироваться надо на 1920×960 — она представительна для кампании, а 240×120 показывает худший случай.

Своя сборка образа

Если нужен образ из текущего состояния репозитория, а не опубликованный:

docker build -t kbc2d docs/theory/2d_solver   --build-arg VERSION=dev --build-arg REVISION=$(git rev-parse --short HEAD)

Рядом лежит docker-compose.yml — то же самое через build:, для локальной отладки.

Без Docker

cargo build --release                 # из docs/theory/2d_solver
cd bench
python preflight.py                   # каждый сценарий стартует на два шага
./run_campaign.sh --calibrate         # замерить MLUPS этой машины
KBC2D_GPU_MLUPS=1400 ./run_campaign.sh --dry-run
./run_campaign.sh --resume

Под Windows то же самое делает run_campaign.ps1 (тот же scenarios.json); он нужен для локальной отладки обвязки, целевая машина — Linux.

Ключи драйвера

ключ что делает
--dry-run печатает смету: что, сколько шагов, сколько часов. Ничего не считает
--calibrate три коротких прогона, замер фактических MLUPS этой машины
--smoke три самых дешёвых прогона: проверить обвязку, а не физику
--resume пропускает прогоны, у которых уже есть summary.json
--group A,B только выбранные группы
--only cyl_re150 по подстроке идентификатора
--budget-hours 24 остановиться, когда время выйдет

Прогон, который упал или развалился, помечается в сводке и не останавливает кампанию: группы E и G специально ищут предел устойчивости, там развал — ожидаемый результат.

Что где лежит

out/summary.csv          сводная таблица: id, группа, статус, секунды, стоимость
out/campaign.log         журнал с отметками времени
out/<id>/cmd.txt         точная команда, которой прогон был запущен
out/<id>/log.txt         полный вывод
out/<id>/report.txt      только итоговый отчёт
out/<id>/series.csv      временные ряды по шагам
out/<id>/summary.json    ключевые метрики машиночитаемо — с этого удобно начинать разбор
out/<id>/*.gif           анимация
out/<id>/*_case.csv      энергия, энстрофия, палинстрофия (эталонные течения)
out/<id>/xt_*.csv        x–t диаграммы (группа G)

Группы

прогонов что проверяется
A 12 эталоны первоисточников: Тейлор–Грин (второй порядок сходимости; при фиксированном u₀ — полка O(Ma²)), сдвиговый слой Re=3·10⁴, затухающая турбулентность
B 16 цилиндр против литературы: стационар Re=20/40, дорожка Re=100…300, разрешение D=16…128, блокировка Ny/D=6…32 с экстраполяцией к бесконечной среде
C 20 модели стенки и субсеточность: hrr / grad / bouzidi / staircase при Re=150 и 2000, плюс сдвиг тела внутри клетки на 0 / ¼ / ½ для всех четырёх
D 14 профили крыла: поляра NACA 0012, Re-серия, изгиб 4412, сходимость по хорде
E 12 сложная и множественная геометрия: тандем, решётка, перфорация, многоэлементный профиль, сверхтонкая пластина, клин, зазубренная кромка
F 9 поле влияния и границы домена: отступы до входа и выхода, вложенный патч
G 14 старт, акустика, время жизни: x–t диаграммы, губки, предел по Re, прогон на 10⁷ шагов
H 9 инварианты: симметрия, зеркальность, зависимость от числа Маха, расхождение f32 против f64
I 9 сверхмелкие сетки 4096×2048 по всем формам и композиции из трёх тел

Стоимость и время

Стоимость каждого прогона хранится в обновлениях узлов (nodes_per_step × steps) — это единственная мера, переносимая между машинами. Часы драйвер получает, поделив её на MLUPS. Оценки по умолчанию исходят из 1200 MLUPS на GPU и 22 на CPU; --calibrate обязателен, потому что эти числа взяты с другой машины.

Пересобрать список с другой длительностью:

python gen_scenarios.py --scale 3      # все прогоны в полтора раза длиннее (≈135 ч)

Что известно заранее

  • Точность f32. Замерено на Тейлоре–Грине: GPU совпадает с CPU/f64, пока истинная ошибка выше ~10⁻³, и промахивается в 40 раз, когда она ниже. Поэтому исследования сходимости из группы A идут на CPU — это указано в самих сценариях.
  • Развалы ожидаемы в группе G (предел по Re) и у прогона A09_shear_n512_lbgk: LBGK при Re=3·10⁴ обязан развалиться там, где KBC доживает — это и есть проверяемое утверждение.
  • Гифки пишутся на всю длительность прогона, в реальном времени, 10 кадр/с. Число кадров этим задано жёстко (у самого длинного прогона их 16 666), поэтому единственный рычаг — размер кадра. Замерено: 0.103 байта на пиксель после LZW; отсюда бюджет кадры×пиксели ≤ 1.5·10⁹ на гифку, но ширина не опускается ниже 480 пикселей. Итог по кампании: ≈4.4 ГБ, самый тяжёлый файл 226 МБ.
  • Перед запуском имеет смысл прогнать предполётную проверку: каждый сценарий стартует на два шага, что ловит опечатки в ключах и несовместимые сочетания до того, как кампания уйдёт считать на девяносто часов. Именно она поймала, что вся группа I падала на пределе GPU в 65535 рабочих групп на измерение.