gen_scenarios.py раскладывает 115 прогонов по пяти долям (LPT, поправка на размер сетки по замерам dzn) — по ≈24 ч каждая; поле shard в scenarios.json. run_campaign.sh/.ps1 получили --shard. Новая точка входа образа entrypoint.sh: при KBC2D_SHARD сама выбирает путь к карте (dzn или NVIDIA), проверяет vulkaninfo и parity.py и запускает долю. Для чужих ПК под Windows/WSL2 — docker-compose.shards.yml и shard.bat. Образ notbigghost/kbc2d:1.3.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
310 lines
23 KiB
Markdown
310 lines
23 KiB
Markdown
# Валидационная кампания
|
||
|
||
115 прогонов, ≈90 машинных часов: 108 на GPU (≈87 часов на RTX 4070 Ti) и 7 на CPU (≈3 часа —
|
||
исследования сходимости группы A, где нужен f64). Проверяет решатель по трём независимым линиям:
|
||
эталонам из статей авторов метода, литературе по обтеканию тел и внутренним инвариантам самой
|
||
схемы. Каждый прогон кладёт в собственную папку `cmd.txt`, `log.txt`, `report.txt`, `series.csv`
|
||
и `summary.json`; гифку пишут **59 прогонов из 115** — остальным `--gif` не передаётся.
|
||
|
||
## Пять долей: чужие ПК под Windows / WSL2
|
||
|
||
Кампания разложена на **пять долей, равных по времени**: у каждого прогона в `scenarios.json`
|
||
есть поле `shard` (1…5). Каждая доля — отдельный контейнер, который при развёртывании **сразу
|
||
начинает считать**: ни сборки, ни ручной цепочки проверок, всё окружение (решатель, Vulkan-
|
||
загрузчик, dzn, Python) — внутри образа.
|
||
|
||
На машину переносятся два файла из корня решателя — `docker-compose.shards.yml` и `shard.bat`:
|
||
|
||
```bat
|
||
shard.bat 3
|
||
```
|
||
|
||
или из любой оболочки:
|
||
|
||
```sh
|
||
docker compose -f docker-compose.shards.yml up -d shard3
|
||
docker compose -f docker-compose.shards.yml logs -f shard3
|
||
```
|
||
|
||
При старте контейнер (`entrypoint.sh`) сам делает то, что раньше делалось профилями check:
|
||
|
||
1. выбирает путь к карте — есть `/dev/dxg`, значит WSL2 / Docker Desktop и dzn; нет —
|
||
нативный Linux с NVIDIA Container Toolkit;
|
||
2. проверяет `vulkaninfo`: нужен хотя бы один не программный адаптер;
|
||
3. гоняет `parity.py` (~2 мин) и только при совпадении с CPU-эталоном идёт дальше; после
|
||
успеха кладёт `out/parity_shardN.ok`, и перезапуск сверку не повторяет;
|
||
4. `run_campaign.sh --resume --shard N`.
|
||
|
||
Любой провал на шагах 1–3 останавливает контейнер с объяснением, что поправить на хосте, — а
|
||
не отдаёт кампанию считать впустую.
|
||
|
||
**В контейнер нельзя положить** драйвер видеокарты: он должен стоять в самой Windows (Vulkan
|
||
внутри контейнера — это dzn, транслирующий вызовы в D3D12 драйвера хоста). И нужен Docker
|
||
Desktop с бэкендом WSL2 (так по умолчанию) либо Docker внутри WSL2.
|
||
|
||
| доля | прогонов | ≈ часов |
|
||
|---|---|---|
|
||
| 1 | 22 | 24.0 |
|
||
| 2 | 23 | 24.0 |
|
||
| 3 | 23 | 24.0 |
|
||
| 4 | 23 | 24.0 |
|
||
| 5 | 24 | 24.0 |
|
||
|
||
Часы — оценка при 1200 MLUPS на крупной сетке **с поправкой на размер сетки**: на dzn мелкие
|
||
сетки считаются медленнее на узел, а сверхмелкие из группы I идут через раздельные привязки
|
||
(кривая — `DZN_MLUPS` в `gen_scenarios.py`, по замерам из раздела «Чего трансляция стоит по
|
||
скорости»). Раскладка жадная: самый долгий прогон — в самую лёгкую долю. Девять прогонов
|
||
группы I (по ≈8.7 ч) разошлись по два на долю в четырёх долях, пятая получила один и добрала остальным.
|
||
Равенство долей — равенство на **одинаковых** картах: на разных доли закончатся в разное время.
|
||
Абсолютные часы на конкретной карте — `--dry-run --shard N` после `--calibrate`.
|
||
|
||
Пересобрать раскладку под другое число машин: `python gen_scenarios.py --shards 3`.
|
||
|
||
**Сбор результатов.** Каждая доля пишет в `./out` рядом с compose-файлом: каталоги прогонов
|
||
`out/<id>/` (уникальны по id), свою сводку `summary_shardN.csv` и свой журнал
|
||
`campaign_shardN.log`. Собрать кампанию целиком — скопировать все пять `out/` в одну папку.
|
||
|
||
## Быстрый старт на сервере
|
||
|
||
Образ опубликован, собирать ничего не нужно: **`notbigghost/kbc2d:1.3.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; на своей карте прогоните сами — две
|
||
минуты.
|
||
|
||
### Предел 128 МиБ на привязку и как он снят
|
||
|
||
dzn объявляет `max_storage_buffer_binding_size = 128 МиБ` (2²⁷ байт), тогда как нативные
|
||
драйверы дают гигабайты. Массив популяций занимает `nx · ny · 9 · 4` байта, так что при
|
||
одной общей привязке потолок выходил **3.73 млн узлов** (примерно 1920×1920), и 14
|
||
прогонов кампании из 115 падали на создании bind group:
|
||
|
||
```
|
||
Buffer binding 0 range 150994944 exceeds `max_*_buffer_binding_size` limit 134217728
|
||
```
|
||
|
||
На эти 14 приходилось 70.7% стоимости кампании — дорогие прогоны как раз крупносеточные.
|
||
|
||
Существенно, что ограничена только **привязка**: `max_buffer_size` у dzn 2047 МиБ, то есть
|
||
буфер держать разрешено, нельзя лишь показать шейдеру его целиком. Поэтому решатель теперь
|
||
умеет показывать тот же буфер **девятью привязками**, по одному направлению в каждой.
|
||
Потолок поднимается в девять раз — до 33.5 млн узлов, чего хватает всей кампании с запасом
|
||
(самая крупная сетка в ней 4096×4096 — 16.8 млн узлов).
|
||
|
||
Вариант выбирается сам, по `max_storage_buffer_binding_size` адаптера: где предела нет,
|
||
собирается прежний общий вариант без `switch` в аксессорах. Проверить оба на одной карте
|
||
можно переменной `KBC2D_SPLIT_POPULATIONS=1` — она включает раздельные привязки
|
||
принудительно.
|
||
|
||
Замерено на Intel Iris Xe, один драйвер, два варианта привязки:
|
||
|
||
| | Cd | energy_end | MLUPS (цилиндр) |
|
||
|---|---|---|---|
|
||
| общая привязка | 2.39486 | 5.74813e-05 | 169.7 |
|
||
| девять привязок | 2.39486 | 5.74813e-05 | 160.0 |
|
||
|
||
Числа совпадают полностью; раздельный вариант стоит **5.7%** пропускной способности, и
|
||
включается только там, где без него счёт вообще невозможен.
|
||
|
||
### Чего трансляция стоит по скорости
|
||
|
||
Плата есть, но она почти вся — накладные расходы на вызов, а не на счёт, и потому падает
|
||
с ростом сетки. Замерено на 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×** |
|
||
| 2048×2048 | 4.19 млн | 111.8 MLUPS | 67.4 MLUPS | **1.66×** |
|
||
|
||
Последняя строка стоит особняком: там уже раздельные привязки (см. ниже), и в плату
|
||
входит их `switch` в аксессорах. Ожидаемый разброс по кампании — от 1.3× до 1.7×.
|
||
|
||
Каждый шаг решателя — несколько отправок в очередь; на мелкой сетке трансляция вызова
|
||
стоит дороже самого счёта, на крупной размазывается. Для кампании это решающее
|
||
обстоятельство, потому что дорогие прогоны в ней как раз крупные:
|
||
|
||
| узлов в сетке | прогонов | доля стоимости кампании |
|
||
|---|---|---|
|
||
| < 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` | остановиться, когда время выйдет |
|
||
| `--shard 3` | только доля 3 (поле `shard` в `scenarios.json`); то же — переменная `KBC2D_SHARD` |
|
||
|
||
Прогон, который упал или развалился, помечается в сводке и **не останавливает кампанию**:
|
||
группы 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` обязателен**,
|
||
потому что эти числа взяты с другой машины.
|
||
|
||
Пересобрать список с другой длительностью:
|
||
|
||
```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 рабочих групп на измерение.
|