# Валидационная кампания 115 прогонов, ≈90 часов на RTX 4070 Ti. Проверяет решатель по трём независимым линиям: эталонам из статей авторов метода, литературе по обтеканию тел и внутренним инвариантам самой схемы. Каждый прогон кладёт логи, ряды, машиночитаемую сводку и гифку в собственную папку. ## Быстрый старт на сервере Образ опубликован, собирать ничего не нужно: **`notbigghost/kbc2d:1.1.0`**. Исходники на сервере тоже не нужны — переносится один файл `docker-compose.server.yml`. ```sh 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 ``` Порядок не случайный: узнать, что карта не видна, лучше на первом шаге, чем через час счёта. Замеренные калибровкой числа подставляются переменными окружения — они влияют только на оценки в часах, не на счёт: ```sh 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 это прописано, но может быть переопределено снаружи. Проверить, что хост вообще умеет отдавать карту: ```sh 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`. ```sh 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 показывает худший случай. ## Своя сборка образа Если нужен образ из текущего состояния репозитория, а не опубликованный: ```sh 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 ```sh 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//cmd.txt точная команда, которой прогон был запущен out//log.txt полный вывод out//report.txt только итоговый отчёт out//series.csv временные ряды по шагам out//summary.json ключевые метрики машиночитаемо — с этого удобно начинать разбор out//*.gif анимация out//*_case.csv энергия, энстрофия, палинстрофия (эталонные течения) out//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` обязателен**, потому что эти числа взяты с другой машины. Пересобрать список с другой длительностью: ```sh 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 рабочих групп на измерение.