Валидационная кампания на 115 прогонов и Docker-образ для сервера
bench/ — список сценариев (порождается gen_scenarios.py, а не правится руками), драйверы под Linux и Windows, предполётная проверка и описание. Девять групп: эталоны первоисточников, цилиндр против литературы, модели стенки и субсеточность, профили крыла, сложная и множественная геометрия, границы домена, старт и время жизни, внутренние инварианты, сверхмелкие сетки до 4096×2048. Стоимость планируется в обновлениях узлов — единственной переносимой между машинами мере; в часы драйвер переводит её по фактическим MLUPS, которые замеряет на месте (--calibrate). Итого 3.76e14 обновлений, ≈90 часов на 4070 Ti. Гифки пишутся на всю длительность прогона в реальном времени, 10 кадр/с. Число кадров этим задано жёстко, поэтому размер регулируется только кадром: замерено 0.103 байта на пиксель после LZW, отсюда бюджет кадры×пиксели с нижней границей по ширине. Итог 4.4 ГБ, самый тяжёлый файл 226 МБ. preflight.py гоняет каждый сценарий на два шага. Окупился сразу: поймал, что вся группа сверхмелких сеток падала на пределе GPU, а девять прогонов группы F передавали --body-x дважды. Оба отказа проявились бы только на сервере, часов через двадцать после старта кампании. Docker: двухстадийная сборка, в образе libvulkan1 и vulkan-tools. NVIDIA Container Toolkit подкладывает Vulkan-ICD только при graphics в NVIDIA_DRIVER_CAPABILITIES — без него wgpu не увидит карту, поэтому capabilities прописаны в образе, а vulkaninfo лежит внутрь для проверки. ENTRYPOINT — драйвер кампании, CMD по умолчанию --dry-run, чтобы случайный docker run не запустил сточасовую задачу. Проверено локально: образ собирается, смоук проходит с монтированием результатов на хост, физика совпадает с хостовой до последней цифры, --backend gpu без карты отказывает явно. GPU-путь в контейнере проверяется только на машине с картой. В README поправлено разделение вкладов в продольную акустику: измерением 2×2 показано, что 74.9% → 1.1% даёт ОДНОРОДНЫЙ СТАРТ, причём при полностью выключенной губке, а губка снимает лишь остаток (1.12% → 0.83% при утроении длины). При старте из покоя губка не помогает совсем — у стоячей четвертьволновой моды там узел давления, где вязкость её не трогает. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,9 @@ target/
|
||||
docs/theory/2d_solver/*.gif
|
||||
docs/theory/2d_solver/*.csv
|
||||
docs/theory/2d_solver/out/
|
||||
# Результаты валидационной кампании — гигабайты гифок и рядов. Сам список сценариев
|
||||
# (bench/scenarios.json) лежит рядом с драйверами и версионируется.
|
||||
docs/theory/2d_solver/bench/out/
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Редакторы и ОС
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Каталог сборки cargo — сотни мегабайт, внутри образа он собирается заново.
|
||||
target/
|
||||
# Результаты прогонов: гифки и ряды монтируются с хоста, в образ не кладутся.
|
||||
bench/out/
|
||||
*.gif
|
||||
*.csv
|
||||
*.json
|
||||
!bench/scenarios.json
|
||||
# Мусор редакторов и ОС
|
||||
.vs/
|
||||
.vscode/
|
||||
.idea/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,51 @@
|
||||
# Образ для развёртывания решателя и валидационной кампании на сервере с NVIDIA.
|
||||
#
|
||||
# Сборка и запуск:
|
||||
# docker build -t kbc2d docs/theory/2d_solver
|
||||
# docker run --rm --gpus all kbc2d --calibrate # проверить, что GPU виден
|
||||
# docker run -d --gpus all -v "$PWD/out:/work/bench/out" --name kbc2d kbc2d --resume
|
||||
#
|
||||
# ГЛАВНАЯ ТОНКОСТЬ — Vulkan в контейнере. NVIDIA Container Toolkit подкладывает внутрь
|
||||
# Vulkan-ICD (nvidia_icd.json) только если в NVIDIA_DRIVER_CAPABILITIES есть `graphics`.
|
||||
# С одним `compute` wgpu не увидит НИ ОДНОГО адаптера и решатель откажется стартовать с
|
||||
# --backend gpu. Поэтому capabilities прописаны в образе, а vulkan-tools положен внутрь,
|
||||
# чтобы первым делом можно было выполнить `vulkaninfo` и убедиться, что карта видна.
|
||||
|
||||
# ── сборка ───────────────────────────────────────────────────────────────────
|
||||
# Версия тулчейна закреплена: та же, на которой решатель собирался и проверялся.
|
||||
FROM rust:1.97-bookworm AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Сначала только манифесты — тогда слой с зависимостями переиспользуется, пока они не менялись.
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
RUN mkdir src && echo 'fn main() {}' > src/main.rs \
|
||||
&& cargo build --release --locked 2>/dev/null || true \
|
||||
&& rm -rf src
|
||||
|
||||
COPY src ./src
|
||||
# touch нужен, чтобы cargo не спутал новые исходники с заглушкой из слоя выше
|
||||
RUN touch src/main.rs && cargo build --release --locked
|
||||
|
||||
# ── исполнение ───────────────────────────────────────────────────────────────
|
||||
FROM debian:bookworm-slim
|
||||
LABEL org.opencontainers.image.title="kbc2d"
|
||||
LABEL org.opencontainers.image.description="Двумерный решатель LBM D2Q9 с энтропийным столкновением KBC"
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libvulkan1 vulkan-tools python3 ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Без graphics в списке возможностей Vulkan-ICD внутрь не попадёт — см. шапку.
|
||||
ENV NVIDIA_VISIBLE_DEVICES=all
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES=compute,utility,graphics
|
||||
|
||||
WORKDIR /work
|
||||
COPY --from=build /src/target/release/kbc2d /work/target/release/kbc2d
|
||||
COPY bench /work/bench
|
||||
RUN chmod +x /work/bench/run_campaign.sh
|
||||
|
||||
# Кампания идёт десятки часов, поэтому по умолчанию образ НЕ запускает её: случайный
|
||||
# `docker run` покажет смету и выйдет.
|
||||
WORKDIR /work/bench
|
||||
ENTRYPOINT ["./run_campaign.sh"]
|
||||
CMD ["--dry-run"]
|
||||
+198
-39
@@ -24,6 +24,9 @@ Multi-Relaxation Models for Simulation of Fluid Turbulence* (arXiv:1507.02509,
|
||||
| `src/main.rs` | **запуск и оркестрирование.** Разбор параметров, сборка постановки, цикл по шагам, живой вывод и итоговый отчёт, выгрузка рядов в CSV. Здесь же контракт `Spec` / `StepRec` / `FieldKind`, общий для обоих бэкендов. |
|
||||
| `src/gif.rs` | **создание гифок.** Тайминг относительно физического времени, палитры, нормировка, служебная надпись, кодирование. |
|
||||
|
||||
Рядом, вне этого разделения: `bench/` — валидационная кампания (список сценариев и драйверы под
|
||||
Linux и Windows), `Dockerfile` с `docker-compose.yml` — образ для развёртывания на сервере.
|
||||
|
||||
## Сборка и запуск
|
||||
|
||||
Нужен Rust 1.75+.
|
||||
@@ -31,10 +34,16 @@ Multi-Relaxation Models for Simulation of Fluid Turbulence* (arXiv:1507.02509,
|
||||
```sh
|
||||
cargo build --release # с GPU-бэкендом
|
||||
cargo build --release --no-default-features # только CPU (без wgpu)
|
||||
cargo test --release # 22 быстрых теста
|
||||
cargo test --release -- --include-ignored # плюс эталоны статьи (~18 с)
|
||||
cargo test --release # 29 быстрых тестов
|
||||
cargo test --release -- --include-ignored # плюс 4 эталона статьи (~18 с)
|
||||
```
|
||||
|
||||
Сборка без GPU кладёт бинарь по тому же пути, поэтому собирать её ПОСЛЕДНЕЙ нельзя: `kbc2d`
|
||||
окажется перезаписан вариантом без wgpu, и `--backend gpu` будет отказывать. Порядок —
|
||||
сначала `--no-default-features`, потом обычная.
|
||||
|
||||
На сервер удобнее ставить образом — см. [Развёртывание](#развёртывание-docker).
|
||||
|
||||
Пример: цилиндр Re=150, гифка завихренности в реальном времени.
|
||||
|
||||
```sh
|
||||
@@ -99,19 +108,28 @@ cargo test --release -- --include-ignored # плюс эталоны стат
|
||||
## Что параметризовано
|
||||
|
||||
- **поток**: скорость (м/с), направление, число Рейнольдса, решёточная скорость (число Маха);
|
||||
- **постановка**: `--case channel` — обтекание тела; `taylor-green`, `shear-layer`,
|
||||
`decaying-turbulence` — периодические эталоны из статей, без тела и граничных условий;
|
||||
- **сетка**: размеры домена и размер ячейки в метрах (плотность сетки), коэффициент измельчения
|
||||
вложенного патча и его границы;
|
||||
- **тело**: семь форм на выбор — `cylinder`, `square`, `diamond`, `ellipse`, `naca`, `triangle`,
|
||||
`plate` — плюс характерный размер, относительная толщина, угол атаки и положение;
|
||||
- **тело**: восемь форм на выбор — `cylinder`, `square`, `diamond`, `ellipse`, `naca`,
|
||||
`triangle`, `plate`, `polygon` — плюс характерный размер, относительная толщина, угол атаки и
|
||||
положение. Профиль задаётся четырёхзначным кодом (`--naca 4412`, с кривизной), произвольный
|
||||
контур — списком вершин (`--poly "x,y;x,y;…"`), **несколько тел сразу** — списком
|
||||
`--bodies "cylinder:d=24,x=120,y=120; naca:d=48,x=260,y=120,a=8"` (сила считается по каждому
|
||||
телу отдельно, до четырёх);
|
||||
- **время**: длительность прогона — либо числом шагов (`--steps`), либо прямо в СЕКУНДАХ
|
||||
физического времени (`--time`, число шагов считается как time/δt); начальное поле (однородный
|
||||
поток либо покой с разгоном), длина разгона, амплитуда и длительность стартового возмущения;
|
||||
- **схема**: оператор столкновения (`kbc`/`bgk`), состав сдвиговой части (`n1`/`n2`), модель
|
||||
стенки на теле (`bouzidi`/`grad`/`staircase`), режим выхода, поглощающая губка, бэкенд,
|
||||
число потоков;
|
||||
стенки на теле (`hrr`/`grad`/`bouzidi`/`staircase`), режим выхода, поглощающие губки перед
|
||||
выходом и после входа, бэкенд, число потоков;
|
||||
- **анимация**: файл, поле (`speed`/`vorticity`/`density`/`gamma`), палитра, масштаб, шаг кадра,
|
||||
частота, скорость воспроизведения, диапазон нормировки;
|
||||
- **вывод**: период живых строк, три уровня подробности, число окон в отчёте о сходимости, CSV.
|
||||
частота, скорость воспроизведения, диапазон нормировки, усреднение k×k клеток в пиксель
|
||||
(`--gif-downsample`, без него кадр с сетки 4096×2048 неподъёмен);
|
||||
- **вывод**: период живых строк, три уровня подробности, число окон в отчёте о сходимости, CSV
|
||||
рядов с прореживанием (`--series-every`), x–t диаграмма осевой линии (`--xt`), метрики
|
||||
эталонных течений (`--case-csv`) и машиночитаемая сводка всего прогона (`--summary`).
|
||||
|
||||
## Что печатает отчёт
|
||||
|
||||
@@ -138,7 +156,7 @@ cargo test --release -- --include-ignored # плюс эталоны стат
|
||||
его дословно: ρ, u → f^eq → s и s^eq → Δs = s − s^eq → **Δh = h − h^eq = f − f^eq − Δs** →
|
||||
γ по замкнутой оценке → **f′ = f − β(2Δs + γΔh)**.
|
||||
|
||||
Проверено тестами (`cargo test`, 22 быстрых + 4 длинных):
|
||||
Проверено тестами (`cargo test`, 29 быстрых + 4 длинных):
|
||||
|
||||
- **проектор Δs** совпадает с матричным `M⁻¹·D·M` в базисе натуральных моментов (6)–(7) до 1e-13
|
||||
— для обоих составов сдвиговой части; идемпотентен, не несёт ни массы, ни импульса;
|
||||
@@ -151,7 +169,12 @@ cargo test --release -- --include-ignored # плюс эталоны стат
|
||||
- **объёмная вязкость по ур. (57)**: ξ = ν при следе в сдвиговой части и ξ = c_s²(1/(γβ) − ½)
|
||||
без него;
|
||||
- равновесие в product-form сохраняет ρ и ρu до 1e-13; Zou–He ставит ровно заданные скорость
|
||||
на входе и плотность на выходе; SDF всех семи форм даёт верный знак и |∇φ| = 1 ± 0.05.
|
||||
на входе и плотность на выходе; SDF всех восьми форм даёт верный знак и |∇φ| = 1 ± 0.05
|
||||
(у эллипса для этого пришлось считать ближайшую точку итеративно: дешёвое приближение давало
|
||||
|∇φ| = 0.57 вдали от поверхности);
|
||||
- **HRR-сборка сохраняет моменты**, ради которых затевалась: ρ, ρu и Π восстановленной функции
|
||||
распределения совпадают с целевыми, а при нулевой скорости она совпадает с Градовой (все
|
||||
коэффициенты 3-го порядка рекурсивно обращаются в ноль).
|
||||
|
||||
### Вихрь Тейлора–Грина: второй порядок сходимости (разд. VI)
|
||||
|
||||
@@ -229,31 +252,45 @@ c_s²(1/(γβ) − ½) и, поскольку измеренная ⟨γ⟩ ≈
|
||||
|
||||
Ключ `--wall`:
|
||||
|
||||
- `bouzidi` (умолчание) — интерполированный отскок: доля пересечения q входит в КАЖДУЮ
|
||||
восстанавливаемую популяцию, полинково;
|
||||
- `grad` — условие Града (Dorschner, Bösch, Chikatamarla, Boulouchos, Karlin, JFM 801 (2016),
|
||||
разд. 2.1 и прил. B): задаются не популяции, а целевые моменты — ρ, u и тензор давлений, —
|
||||
после чего недостающие популяции собираются приближением Града (2.13);
|
||||
- `hrr` (умолчание) — восстановление по целевым моментам с **рекурсивной регуляризацией**
|
||||
(Malaspinas 2015; Coreixas и др., PRE 96, 033306): ряд Эрмита продолжен до 3-го порядка, а
|
||||
коэффициенты 3-го порядка не считаются по популяциям, а выражаются через 2-й рекурсивно:
|
||||
`a₃_xxy = 2u_x·a₂_xy + u_y·a₂_xx`, `a₃_xyy = 2u_y·a₂_xy + u_x·a₂_yy`. В D2Q9 `a₃_xxx` и
|
||||
`a₃_yyy` решёткой не поддерживаются и отбрасываются; множитель 1/2c_s⁶ (а не 1/6c_s⁶) учитывает
|
||||
три перестановки индексов;
|
||||
- `grad` — то же самое с обрывом ряда на тензоре давлений: условие Града (Dorschner, Bösch,
|
||||
Chikatamarla, Boulouchos, Karlin, JFM 801 (2016), разд. 2.1 и прил. B). Задаются не популяции,
|
||||
а целевые моменты — ρ, u и Π, — после чего недостающие популяции собираются приближением
|
||||
Града (2.13);
|
||||
- `bouzidi` — интерполированный отскок: доля пересечения q входит в КАЖДУЮ восстанавливаемую
|
||||
популяцию, полинково;
|
||||
- `staircase` — простой отскок, q игнорируется. Не для счёта: это база сравнения, показывающая,
|
||||
сколько именно даёт субсеточность.
|
||||
|
||||
Целевые моменты у `hrr` и `grad` одни и те же — (B 1) и (B 3) прил. B JFM 801; отличается только
|
||||
то, до какого порядка восстанавливается функция распределения по этим моментам.
|
||||
|
||||
**Субсеточность — измеренная.** Прямой тест: сдвигаем тело внутри клетки и смотрим, насколько
|
||||
поедет Cd. У по-настоящему субсеточной границы ответ не должен зависеть от того, где тело стоит
|
||||
относительно узлов (Re = 20, D = 16, стационар, сдвиги 0…½ клетки):
|
||||
относительно узлов (Re = 20, D = 16, стационар, пять положений на полклетки, GPU):
|
||||
|
||||
| модель | разброс Cd | Cd |
|
||||
|---|---|---|
|
||||
| `staircase` | 1.11% | 2.68–2.71 |
|
||||
| `grad` | 0.64% | 2.64–2.66 |
|
||||
| `bouzidi` | **0.19%** | 2.653–2.658 |
|
||||
| `staircase` | 0.98% | 2.482–2.506 |
|
||||
| `grad` | 0.62% | 2.449–2.464 |
|
||||
| `hrr` | 0.61% | 2.450–2.464 |
|
||||
| `bouzidi` | **0.14%** | 2.459–2.463 |
|
||||
|
||||
Град оказывается ровно между ступенькой и Bouzidi, и это следует из его устройства: положение
|
||||
**HRR не улучшает разрешение геометрии и не должен** — 0.61% против 0.62% у Града. Это следует
|
||||
из устройства обеих схем: третий порядок Эрмита уточняет ВОССТАНОВЛЕНИЕ популяций по моментам, а
|
||||
положение стенки входит в моментные схемы совсем другим местом. Обе моментные схемы оказываются
|
||||
ровно между ступенькой и Bouzidi, и это тоже следует из их устройства: положение
|
||||
стенки входит туда ТОЛЬКО через целевую скорость (B 1) — одну усреднённую по узлу величину.
|
||||
Целевая плотность (B 3) — обычная сумма отскочивших и известных популяций, без q вовсе; тензор
|
||||
давлений — конечные разности по решётке, тоже без q. Плюс все недостающие популяции узла
|
||||
собираются из ОДНОГО набора моментов, так что полинковая направленность теряется. Bouzidi же
|
||||
подставляет свою q в каждую популяцию отдельно. Ступенчатой поверхность у Града не становится,
|
||||
но геометрия у него разрешена заметно грубее.
|
||||
подставляет свою q в каждую популяцию отдельно. Ступенчатой поверхность у моментных схем не
|
||||
становится, но геометрия у них разрешена заметно грубее.
|
||||
|
||||
**Сходимость по разрешению тела.** Физическая постановка фиксирована (домен 15D × 10D,
|
||||
блокировка 0.1, Re = 20), меняется только число клеток на диаметр:
|
||||
@@ -269,27 +306,67 @@ c_s²(1/(γβ) − ½) и, поскольку измеренная ⟨γ⟩ ≈
|
||||
Для сравнения, `staircase` при D = 16 даёт 2.69 — то есть +6.7% к пределу, тогда как обе
|
||||
субсеточные модели держатся в пределах +0.4%.
|
||||
|
||||
**Зачем тогда Град.** Его преимущество в статье — не геометрическая точность, а устойчивость
|
||||
на турбулентных режимах (авторы пишут, что интерполяционные схемы «ограничены низкими числами
|
||||
Рейнольдса, поскольку на границе возникают паразитные скачки») и естественная форма для
|
||||
подвижных стенок: скорость стенки входит в целевые значения, а не отдельной поправкой.
|
||||
**Зачем тогда моментные схемы.** Их преимущество в статье — не геометрическая точность, а
|
||||
устойчивость на турбулентных режимах (авторы пишут, что интерполяционные схемы «ограничены
|
||||
низкими числами Рейнольдса, поскольку на границе возникают паразитные скачки») и естественная
|
||||
форма для подвижных стенок: скорость стенки входит в целевые значения, а не отдельной поправкой.
|
||||
В здешней канальной постановке преимущества по устойчивости воспроизвести не удалось: при росте
|
||||
Re обе модели теряют счёт на одном и том же значении (Re ≈ 5·10⁴ при теле в 16 клеток), то есть
|
||||
ограничивает не стенка, а что-то другое — вероятнее всего Zou–He при τ → ½. Поэтому умолчание —
|
||||
`bouzidi`, а `grad` стоит держать в виду для будущих подвижных тел.
|
||||
ограничивает не стенка, а что-то другое — вероятнее всего Zou–He при τ → ½.
|
||||
|
||||
Ограничение реализации: `--wall grad` пока только на процессорном бэкенде; GPU при таком выборе
|
||||
отказывается запускаться явно, а не считает молча по Bouzidi.
|
||||
**Умолчание — `hrr`, и это решение временное.** Оно принято по устройству схемы (третий порядок
|
||||
Эрмита фильтрует высокочастотный мусор у стенки, чего обрыв на Π не делает), а не по здешним
|
||||
измерениям: на стационарном цилиндре при Re = 20 отличить `hrr` от `grad` нельзя вовсе. Вопрос
|
||||
ставит ребром группа C кампании — там обе моментные схемы и Bouzidi гоняются на Re = 20, 150 и
|
||||
2000. Если данные не подтвердят преимущества HRR на турбулентном режиме, умолчанием станет
|
||||
`bouzidi`, у которого измеренное разрешение геометрии вчетверо лучше.
|
||||
|
||||
Все четыре модели работают на обоих бэкендах и совпадают между ними до 0.007% по Cd. Это
|
||||
специально проверяется: раньше GPU при `--wall staircase` молча считал по Bouzidi, и обнаружилось
|
||||
это только потому, что две модели дали побитово одинаковый результат там, где обязаны были
|
||||
разойтись.
|
||||
|
||||
### Паритет бэкендов и согласованность уровней
|
||||
|
||||
CPU (f64) и GPU (f32) на одной постановке совпадают до 4–5 значащих цифр шаг в шаг: ⟨ρ⟩
|
||||
1.04933 против 1.04934, Cd 2.339 против 2.338, ⟨γ⟩ 1.2645 против 1.2646. На Intel Iris Xe GPU
|
||||
даёт ≈105 MLUPS против ≈18 MLUPS у процессора.
|
||||
даёт ≈195 MLUPS против ≈18 MLUPS у процессора.
|
||||
|
||||
Один и тот же случай с патчем ×2 и вовсе без измельчения (`--refine 1`) даёт St 0.1951 против
|
||||
0.1970 и ⟨Cd⟩ 1.956 против 1.943 — связка уровней систематики не вносит.
|
||||
|
||||
**Предел на размер сетки снят.** У GPU есть жёсткий предел `maxComputeWorkgroupsPerDimension`
|
||||
= 65535, а диспетчеризация была одномерной: при 64 узлах на рабочую группу это упирало сетку в
|
||||
4.2 миллиона узлов, то есть примерно 2048×2048. Всё, что крупнее, падало ошибкой валидации —
|
||||
не считало медленно, а не запускалось вовсе. Теперь диспетчеризация двумерная, а линейный
|
||||
индекс собирается в шейдере (`lin()`/`wlin()`); отображение «рабочая группа → узлы» при этом
|
||||
остаётся ровно линейным, поэтому редукции ничего не заметили. Проверено до 4096×4096, паритет
|
||||
с CPU не сдвинулся ни в одной цифре.
|
||||
|
||||
**Где именно кончается f32.** Прямой замер на Тейлоре–Грине, где ошибка известна точно:
|
||||
|
||||
| N | CPU, f64 | GPU, f32 |
|
||||
|---|---|---|
|
||||
| 64 | 9.83e-3 | 9.80e-3 |
|
||||
| 128 | 2.40e-3 | 3.29e-3 |
|
||||
| 256 | 5.97e-4 | 2.37e-2 |
|
||||
|
||||
Пока истинная ошибка выше ~10⁻³, f32 идёт с f64 вровень; ниже — промахивается на порядок и
|
||||
больше. Практический вывод, заложенный в кампанию: исследования сходимости считаются на CPU,
|
||||
всё остальное — на GPU. Двойной точности на GPU здесь быть не может в принципе: **в WGSL типа
|
||||
`f64` не существует**, поэтому wgpu не выразит её ни на каком железе; локальная Iris Xe вдобавок
|
||||
сообщает `shaderFloat64 = false`, а на потребительских NVIDIA f64 идёт в 1/64 от f32 — то есть
|
||||
медленнее, чем CPU.
|
||||
|
||||
Что удалось выжать вместо точности — производительность. Два изменения:
|
||||
|
||||
1. **Батчинг чтения.** Раньше после каждого шага делался `map_async` + `poll(Wait)` ради 48 байт
|
||||
статистики: на 240×120 счёт упирался в 863 шаг/с при том, что сам счёт занимал 0.27 мс из
|
||||
1.16. Теперь итоги копятся в кольце на 128 слотов, синхронизация — раз в батч: **6715 шаг/с**,
|
||||
в 7.8 раза быстрее, при неизменном пошаговом интерфейсе снаружи.
|
||||
2. **Компенсированное суммирование** (Кэхена–Ноймайера) в редукциях и в сумме сил. Наивная сумма
|
||||
по 10⁵–10⁷ узлам съедает ~log₂N бит мантиссы — именно там f32 терял основную точность.
|
||||
|
||||
### Обтекание цилиндра против литературы
|
||||
|
||||
Постановка 480×240, D = 24, Re = 150, блокировка β = D/Ny = 0.1, умолчания решателя:
|
||||
@@ -341,22 +418,101 @@ CPU (f64) и GPU (f32) на одной постановке совпадают
|
||||
Старт из покоя (`--init rest`) разгоняет весь столб жидкости и закачивает моду; при разгоне
|
||||
за 1000 шагов против акустического пробега 831 шаг это для звука удар.
|
||||
- **губка перед выходом включена и подобрана по домену** (`nx/12`, не меньше 16 столбцов,
|
||||
с запасом до патча). Отключается `--sponge-len 0`.
|
||||
с запасом до патча). Отключается `--sponge-len 0`. Вклад у неё скромный — см. таблицу ниже,
|
||||
— но она бесплатна для сил и снимает остаточную пульсацию примерно вдвое при утроении длины.
|
||||
- **`--outlet extrapolate`** — нуль-градиент поперечной скорости; жёсткий ноль отражает вихри
|
||||
дорожки обратно к телу.
|
||||
|
||||
Эффект умолчаний на той же постановке:
|
||||
**Что именно помогает — разделено измерением** (480×240, D=24, Re=150, 30 000 шагов):
|
||||
|
||||
| | размах ⟨ρ⟩ | пульсация u′/U |
|
||||
|---|---|---|
|
||||
| старт из покоя, без губки | 0.1307 | **75%** |
|
||||
| умолчания | 0.0017 | **1.0%** |
|
||||
| старт | губка, столбцов | пульсация u′/U | St | ⟨Cd⟩ | rms Cl |
|
||||
|---|---|---|---|---|---|
|
||||
| uniform | 0 | 1.12% | 0.1839 | 1.483 | 0.404 |
|
||||
| uniform | 40 | 1.03% | 0.1840 | 1.483 | 0.404 |
|
||||
| uniform | 120 | 0.83% | 0.1839 | 1.482 | 0.403 |
|
||||
| rest | 0 | **74.91%** | 0.1534 | 2.009 | 0.807 |
|
||||
| rest | 40 | **74.90%** | 0.1539 | 2.010 | 0.808 |
|
||||
| rest | 120 | **74.99%** | 0.1542 | 2.008 | 0.808 |
|
||||
|
||||
Заодно выправились и сами числа: St 0.183 вместо 0.194, rms Cl 0.38 вместо 0.84, ⟨ρ⟩ 1.0000.
|
||||
Читается однозначно: **весь эффект даёт однородный старт**, 74.9% → 1.12%, причём при
|
||||
полностью выключенной губке. Губка снимает только остаток — 1.12% → 1.03% → 0.83%, — и на
|
||||
силы с частотой схода не влияет вовсе (St 0.1839 во всех трёх строках).
|
||||
|
||||
**При старте из покоя губка не помогает совсем.** Это не осечка реализации, а свойство моды:
|
||||
губка поднимает вязкость на последних столбцах, а там у стоячей четвертьволновой моды **узел
|
||||
давления и пучность скорости** — то самое место, где повышенная вязкость её почти не трогает.
|
||||
Бегущую волну такая губка съедает, стоячую — нет. Убрать моду можно только не возбуждая её.
|
||||
|
||||
Заодно видно, ЧЕМ платит неверный старт: St 0.153 вместо 0.184, ⟨Cd⟩ 2.01 вместо 1.48,
|
||||
rms Cl 0.81 вместо 0.40 — то есть 75-процентная продольная пульсация ломает не косметику, а
|
||||
все три величины, ради которых постановка и считается.
|
||||
|
||||
Отчёт печатает период моды, время её вязкого затухания и измеренную пульсацию в конце прогона,
|
||||
с явным предупреждением, если она превысила 5% от U.
|
||||
|
||||
**Инструмент разбора — `--xt`.** Каждые `--xt-every` шагов пишется срез ⟨ρ⟩ и u_x вдоль осевой
|
||||
линии, по строке на срез. Наклон полос на такой диаграмме прямо даёт скорость распространения:
|
||||
звук (±c_s) или конвекция (U). Стоячие узлы видны как вертикальные линии постоянной фазы, и это
|
||||
сразу отличает резонанс от неустойчивости самого граничного условия, привязанной к столбцу x = 0
|
||||
и никуда не бегущей.
|
||||
|
||||
Есть и **губка после входа** — `--sponge-in <столбцов>`, по умолчанию выключена. Гасит продольные
|
||||
волны до того, как они отразятся от входа-поршня. Осмысленна на высоких Re, где стартовая волна
|
||||
перестаёт быть безобидной полоской; цена — искажение профиля прямо на входе, поэтому включать её
|
||||
надо осознанно, а не «на всякий случай».
|
||||
|
||||
Что **не помогает** и оставлено только для повторной проверки — `--init-taper`. Замерено:
|
||||
сглаживание стартовой скорости у тела давит возмущение плотности на первом шаге восьмикратно
|
||||
(2.54·10⁻² → 3.13·10⁻³), но пик ЗА ПРОГОН при этом даже подрастает (до 3.22·10⁻²). Возмущение
|
||||
просто переносится во времени: поток всё равно обязан разогнаться вокруг тела, и энергия этого
|
||||
переходного процесса задана физикой, а не гладкостью начального поля. Умолчание — 0.
|
||||
|
||||
## Валидационная кампания
|
||||
|
||||
`bench/` — 115 прогонов на ≈90 часов GPU, разложенных по девяти группам: эталоны первоисточников,
|
||||
цилиндр против литературы, модели стенки, профили крыла, сложная и множественная геометрия,
|
||||
границы домена, старт и время жизни, внутренние инварианты, сверхмелкие сетки до 4096×2048.
|
||||
Каждый прогон кладёт логи, ряды, машиночитаемую сводку и гифку на всю свою длительность в
|
||||
собственную папку.
|
||||
|
||||
```sh
|
||||
cd bench
|
||||
python preflight.py # каждый сценарий стартует на два шага: ловит опечатки
|
||||
./run_campaign.sh --calibrate # замерить MLUPS этой машины: оценки в часах иначе гадание
|
||||
./run_campaign.sh --dry-run # смета: что, сколько шагов, сколько часов
|
||||
./run_campaign.sh --resume # считать, пропуская уже готовое
|
||||
```
|
||||
|
||||
Предполётную проверку стоит гонять всерьёз: она поймала, что вся группа сверхмелких сеток
|
||||
падала на пределе GPU (см. ниже), а девять прогонов передавали `--body-x` дважды. Оба отказа
|
||||
проявились бы только на сервере, часов через двадцать после старта кампании.
|
||||
|
||||
Подробности — в `bench/README.md`: раскладка выходных файлов, таблица групп, модель стоимости и
|
||||
список того, что известно заранее (какие прогоны обязаны развалиться и почему).
|
||||
|
||||
## Развёртывание (Docker)
|
||||
|
||||
```sh
|
||||
docker build -t kbc2d docs/theory/2d_solver
|
||||
docker run --rm --gpus all kbc2d --calibrate # проверить, что GPU виден
|
||||
docker run -d --gpus all -v "$PWD/out:/work/bench/out" kbc2d --resume
|
||||
```
|
||||
|
||||
`ENTRYPOINT` — драйвер кампании, `CMD` по умолчанию `--dry-run`: случайный `docker run` покажет
|
||||
смету и выйдет, а не запустит сточасовую задачу.
|
||||
|
||||
**Главная тонкость — Vulkan внутри контейнера.** NVIDIA Container Toolkit подкладывает
|
||||
Vulkan-ICD (`nvidia_icd.json`) только если в `NVIDIA_DRIVER_CAPABILITIES` есть `graphics`;
|
||||
с одним `compute` wgpu не увидит ни одного адаптера. В образе это прописано, но может быть
|
||||
переопределено снаружи, поэтому `vulkan-tools` лежит внутрь: первым делом на сервере стоит
|
||||
выполнить `docker run --rm --gpus all --entrypoint vulkaninfo kbc2d --summary`.
|
||||
|
||||
Проверено локально: образ собирается, кампания внутри него проходит смоук с монтированием
|
||||
результатов на хост, физика совпадает с хостовой до последней цифры (ошибка Тейлора–Грина
|
||||
9.829e-3 при N=64 и 2.401e-3 при N=128 — те же значения, что вне контейнера), а `--backend gpu`
|
||||
без проброшенной карты отказывает явным сообщением, а не считает молча. GPU-путь в контейнере
|
||||
проверяется только на машине с картой.
|
||||
|
||||
## Дальше
|
||||
|
||||
- σ·n-кросс-чек силы (интеграл тензора напряжений по контуру) как независимая проверка GMEM;
|
||||
@@ -365,4 +521,7 @@ CPU (f64) и GPU (f32) на одной постановке совпадают
|
||||
стартовый импульс от появления тела в потоке;
|
||||
- подвижные и вращающиеся тела: GMEM уже записан в галилей-инвариантной форме и принимает
|
||||
скорость стенки на линке, но подача этой скорости не подключена;
|
||||
- несколько тел одновременно (сейчас — одно тело на выбор из семи форм).
|
||||
- разбор нерешённых 12% по Cd и предела устойчивости Re ≈ 5·10⁴ — обе задачи вынесены в
|
||||
кампанию (группы B и G), выводы делать по её данным;
|
||||
- больше четырёх тел в домене: сейчас сила считается по четырём вёдрам (`MAX_BODY_BUCKETS`),
|
||||
геометрия при этом собирается из любого числа тел, но силы сверх четвёртого сливаются вместе.
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# Валидационная кампания
|
||||
|
||||
115 прогонов, ≈90 часов на RTX 4070 Ti. Проверяет решатель по трём независимым линиям:
|
||||
эталонам из статей авторов метода, литературе по обтеканию тел и внутренним инвариантам самой
|
||||
схемы. Каждый прогон кладёт логи, ряды, машиночитаемую сводку и гифку в собственную папку.
|
||||
|
||||
## Быстрый старт на сервере
|
||||
|
||||
```sh
|
||||
docker build -t kbc2d docs/theory/2d_solver
|
||||
docker run --rm --gpus all --entrypoint python3 kbc2d preflight.py # сценарии стартуют
|
||||
docker run --rm --gpus all kbc2d --calibrate # GPU виден
|
||||
docker run -d --gpus all -v "$PWD/out:/work/bench/out" --name kbc2d kbc2d --resume
|
||||
docker logs -f kbc2d
|
||||
```
|
||||
|
||||
Первым делом стоит убедиться, что Vulkan внутри контейнера действительно видит карту:
|
||||
|
||||
```sh
|
||||
docker run --rm --gpus all --entrypoint vulkaninfo kbc2d --summary | head -20
|
||||
```
|
||||
|
||||
Если адаптеров ноль — почти наверняка дело в `NVIDIA_DRIVER_CAPABILITIES`: Container Toolkit
|
||||
подкладывает Vulkan-ICD только при наличии `graphics` в списке. В образе это прописано, но
|
||||
может быть переопределено снаружи.
|
||||
|
||||
## Без 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/<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 рабочих групп на измерение.
|
||||
@@ -0,0 +1,518 @@
|
||||
#!/usr/bin/env python3
|
||||
# Генератор списка прогонов кампании -> scenarios.json
|
||||
#
|
||||
# Список порождается кодом, а не правится руками: серии по Re, по разрешению и по углу атаки
|
||||
# получаются циклами, а стоимость каждого прогона считается тут же и суммируется. Запуск:
|
||||
# python gen_scenarios.py # перезаписать scenarios.json и напечатать смету
|
||||
# python gen_scenarios.py --scale 2 # растянуть все длительности вдвое
|
||||
#
|
||||
# Стоимость меряется в ОБНОВЛЕНИЯХ УЗЛОВ (nodes_per_step * steps) — единственная переносимая
|
||||
# между машинами мера. Часы драйвер получает из неё, поделив на фактические MLUPS, которые
|
||||
# замеряет на месте (--calibrate).
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
|
||||
RUNS = []
|
||||
U_LAT = 0.05 # решёточная скорость по умолчанию
|
||||
DX = 0.1 # размер клетки, м (умолчание решателя)
|
||||
U_PHYS = 30.0 # скорость потока, м/с (умолчание решателя)
|
||||
GIF_MAX_W = 1280 # предельная ширина кадра после прореживания
|
||||
GIF_MIN_W = 480 # ниже не опускаемся: нечитаемая гифка бесполезнее большой
|
||||
GIF_PX_BUDGET = 1.5e9 # кадры×пиксели на гифку; при 0.103 байта на пиксель это ~150 МБ
|
||||
|
||||
|
||||
def conv_steps(d_cells, n_conv, u_lat=U_LAT):
|
||||
"""Сколько шагов нужно на n_conv конвективных времён D/U."""
|
||||
return int(round(n_conv * d_cells / u_lat))
|
||||
|
||||
|
||||
def add(rid, group, title, expect, args, nodes, steps, backend="gpu",
|
||||
gif=None, nx=None, extra_out=()):
|
||||
"""Записать прогон. `nodes` — обновлений узлов за шаг, `steps` — сколько шагов."""
|
||||
a = list(args) + ["--steps", str(steps), "--backend", backend]
|
||||
# ряды прореживаем так, чтобы их осталось порядка 200 тысяч записей
|
||||
if steps > 400_000:
|
||||
a += ["--series-every", str(max(1, steps // 200_000))]
|
||||
if "case_csv" in extra_out:
|
||||
a += ["--case-csv", f"{rid}_case.csv"]
|
||||
if gif:
|
||||
# Гифка идёт на всю длительность прогона в реальном времени, 10 кадр/с — число кадров
|
||||
# задано физикой прогона и не обсуждается. Единственный рычаг — пиксели в кадре, и он
|
||||
# нужен: замерено 0.103 байта на пиксель после LZW, так что прогон на 16 тысяч кадров
|
||||
# при 960×576 весит 900 МБ, а такой файл уже нечем открыть. Поэтому кроме ограничения
|
||||
# по ширине действует бюджет ПРОИЗВЕДЕНИЯ кадры×пиксели, но ширина не опускается ниже
|
||||
# GIF_MIN_W: нечитаемая гифка бесполезнее большой.
|
||||
ny = max(1, int(nodes) // max(1, nx or 1))
|
||||
u_lat = float(a[a.index("--u-lat") + 1]) if "--u-lat" in a else U_LAT
|
||||
frames = max(1, int(steps * (u_lat * DX / U_PHYS) * 10))
|
||||
down = max(1, math.ceil((nx or 0) / GIF_MAX_W))
|
||||
while nx and frames * (nx // down) * (ny // down) > GIF_PX_BUDGET \
|
||||
and nx // (down + 1) >= GIF_MIN_W:
|
||||
down += 1
|
||||
a += ["--gif", gif, "--gif-field", "vorticity", "--gif-every", "auto",
|
||||
"--gif-fps", "10", "--gif-speed", "1",
|
||||
"--gif-downsample", str(down), "--gif-scale", "1"]
|
||||
RUNS.append({
|
||||
"id": rid, "group": group, "title": title, "expect": expect,
|
||||
"backend": backend, "args": a,
|
||||
"nodes_per_step": int(nodes), "steps": int(steps),
|
||||
"cost": int(nodes) * int(steps),
|
||||
"outputs": list(extra_out),
|
||||
})
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# A. Эталоны первоисточников: периодические течения, тела и ГУ нет вовсе
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
def group_a(scale):
|
||||
NU = 0.0192
|
||||
for n in (64, 128, 256, 512):
|
||||
# диффузионное измельчение: nu фиксирована, u0 ~ 1/N, поэтому Re сохраняется, а
|
||||
# число Маха падает вместе с сеткой — только так виден второй порядок целиком
|
||||
u0 = 0.03 * 64 / n
|
||||
tc = int(round(math.log(2.0) / (NU * (2 * math.pi / n) ** 2 * 17)))
|
||||
add(f"A{len(RUNS)+1:02d}_tg_diff_n{n}", "A",
|
||||
f"Тейлор–Грин, диффузионное измельчение, N={n} (CPU/f64)",
|
||||
"порядок сходимости 2 по ряду N=64..512; ошибка на N=512 ниже 2e-4",
|
||||
["--case", "taylor-green", "--nx", str(n), "--ny", str(n), "--refine", "1",
|
||||
"--u-lat", f"{u0:.6f}", "--re", "100", "--case-every", str(max(1, tc // 20))],
|
||||
n * n, tc, backend="cpu", extra_out=("case_csv",))
|
||||
|
||||
for n in (128, 256):
|
||||
# при ФИКСИРОВАННОМ u0 ошибка упирается в полку O(Ma^2) и от сетки не зависит
|
||||
tc = int(round(math.log(2.0) / (NU * (2 * math.pi / n) ** 2 * 17)))
|
||||
add(f"A{len(RUNS)+1:02d}_tg_fixed_n{n}", "A",
|
||||
f"Тейлор–Грин при фиксированном u₀, N={n} — демонстрация полки O(Ma²)",
|
||||
"ошибка перестаёт падать с ростом N: это свойство слабо-сжимаемого метода",
|
||||
["--case", "taylor-green", "--nx", str(n), "--ny", str(n), "--refine", "1",
|
||||
"--u-lat", "0.03", "--re", f"{0.03 * n / NU:.1f}",
|
||||
"--case-every", str(max(1, tc // 20))],
|
||||
n * n, tc, backend="cpu", extra_out=("case_csv",))
|
||||
|
||||
for n in (512,):
|
||||
for op, km, tag in (("kbc", "n1", "kbc_n1"), ("kbc", "n2", "kbc_n2"), ("bgk", "n1", "lbgk")):
|
||||
steps = int(4 * n / 0.04 * scale)
|
||||
add(f"A{len(RUNS)+1:02d}_shear_n{n}_{tag}", "A",
|
||||
f"Сдвиговый слой Re=3·10⁴, N={n}, {tag}",
|
||||
"KBC доживает до конца, LBGK обязан развалиться (разд. VII статьи)",
|
||||
["--case", "shear-layer", "--nx", str(n), "--ny", str(n), "--refine", "1",
|
||||
"--u-lat", "0.04", "--re", "30000", "--collision", op, "--kbc-model", km,
|
||||
"--case-every", str(max(1, steps // 200))],
|
||||
n * n, steps, gif=f"shear_{tag}.gif", nx=n, extra_out=("case_csv",))
|
||||
|
||||
for n, tag, ops in ((2048, "n2048", ("kbc", "bgk")), (4096, "n4096", ("kbc",))):
|
||||
for op in ops:
|
||||
steps = int(200_000 * scale * (2048 / n))
|
||||
add(f"A{len(RUNS)+1:02d}_turb_{tag}_{op}", "A",
|
||||
f"Затухающая турбулентность N={n}, {op}",
|
||||
"энстрофия и палинстрофия падают монотонно; KBC держится там, где LBGK нет",
|
||||
["--case", "decaying-turbulence", "--nx", str(n), "--ny", str(n), "--refine", "1",
|
||||
"--u-lat", "0.02", "--re", f"{0.02 * n / 8.16e-5:.0f}", "--collision", op,
|
||||
"--case-every", str(max(1, steps // 300))],
|
||||
n * n, steps, gif=f"turb_{tag}_{op}.gif", nx=n, extra_out=("case_csv",))
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# B. Цилиндр против литературы
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
def flatten(d):
|
||||
"""Словарь ключей в плоский argv. Именно словарь, а не список: clap отвергает
|
||||
повторённый ключ, а сценарии сплошь и рядом переопределяют умолчания сборщика."""
|
||||
a = []
|
||||
for k, v in d.items():
|
||||
a += [k, str(v)]
|
||||
return a
|
||||
|
||||
|
||||
def cyl_args(d, re, nx, ny, **kw):
|
||||
a = {"--shape": "cylinder", "--size": d, "--nx": nx, "--ny": ny,
|
||||
"--body-x": nx // 5, "--re": re, "--refine": 1}
|
||||
a.update({"--" + k.replace("_", "-"): v for k, v in kw.items()})
|
||||
return flatten(a)
|
||||
|
||||
|
||||
def group_b(scale):
|
||||
# стационарные режимы: дорожки нет, Cd — одно число
|
||||
for re in (20, 40):
|
||||
d, nx, ny = 48, 960, 576
|
||||
st = int(conv_steps(d, 60) * scale)
|
||||
add(f"B{len(RUNS)-len(RUNS)+len([r for r in RUNS if r['group']=='B'])+1:02d}_cyl_re{re}",
|
||||
"B", f"Цилиндр Re={re}, стационар, D=48",
|
||||
"Cd выходит на постоянную; сравнение с литературой по стационарному обтеканию",
|
||||
cyl_args(d, re, nx, ny, pert_amp=0), nx * ny, st,
|
||||
gif=f"cyl_re{re}.gif", nx=nx)
|
||||
|
||||
# дорожка Кармана: длинное осреднение, это главный заход на литературу
|
||||
for re in (100, 150, 200, 300):
|
||||
d, nx, ny = 64, 1280, 768
|
||||
st = int(conv_steps(d, 1500) * scale)
|
||||
add(f"B{len([r for r in RUNS if r['group']=='B'])+1:02d}_cyl_re{re}", "B",
|
||||
f"Цилиндр Re={re}, дорожка Кармана, D=64, 1500 конв. времён",
|
||||
"St≈0.183 и ⟨Cd⟩≈1.33 при Re=150 после поправки на блокировку",
|
||||
cyl_args(d, re, nx, ny), nx * ny, st, gif=f"cyl_re{re}.gif", nx=nx)
|
||||
|
||||
# разрешение тела: сходимость Cd
|
||||
for d in (16, 32, 64, 128):
|
||||
nx, ny = 20 * d, 12 * d
|
||||
st = int(conv_steps(d, 800) * scale)
|
||||
add(f"B{len([r for r in RUNS if r['group']=='B'])+1:02d}_cyl_d{d}", "B",
|
||||
f"Цилиндр Re=150, разрешение D={d}",
|
||||
"Cd сходится по D; экстраполяция даёт сеточно-независимое значение",
|
||||
cyl_args(d, 150, nx, ny), nx * ny, st, gif=f"cyl_d{d}.gif", nx=nx)
|
||||
|
||||
# блокировка канала: экстраполяция к бесконечной среде — прямой заход на нерешённые 12%
|
||||
for k in (6, 8, 12, 16, 24, 32):
|
||||
d, nx, ny = 48, 960, k * 48
|
||||
st = int(conv_steps(d, 1000) * scale)
|
||||
add(f"B{len([r for r in RUNS if r['group']=='B'])+1:02d}_cyl_block{k}", "B",
|
||||
f"Цилиндр Re=150, блокировка Ny/D={k}",
|
||||
"экстраполяция Cd и St к нулевой блокировке; ожидание — сходимость к 1.33 и 0.183",
|
||||
cyl_args(d, 150, nx, ny), nx * ny, st, gif=None, nx=nx)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# C. Модели стенки и субсеточность
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
def group_c(scale):
|
||||
for wall in ("hrr", "grad", "bouzidi", "staircase"):
|
||||
for re in (150, 2000):
|
||||
d, nx, ny = 32, 640, 384
|
||||
st = int(conv_steps(d, 400 if re > 20 else 60) * scale)
|
||||
add(f"C{len([r for r in RUNS if r['group']=='C'])+1:02d}_wall_{wall}_re{re}", "C",
|
||||
f"Модель стенки {wall}, Re={re}",
|
||||
"все субсеточные модели обязаны сойтись к одному пределу; staircase — база",
|
||||
cyl_args(d, re, nx, ny, wall=wall), nx * ny, st,
|
||||
gif=f"wall_{wall}_re{re}.gif" if re == 150 else None, nx=nx)
|
||||
|
||||
# Чувствительность к положению тела ВНУТРИ клетки — прямая проверка субсеточности, и
|
||||
# единственное измерение, которое вообще разделяет модели стенки. Двух положений для этого
|
||||
# мало: по двум точкам не отличить систематический разброс от совпадения, поэтому берём
|
||||
# четверти, и все четыре модели. Прогоны короткие (стационар при Re=20), вся серия — минуты.
|
||||
for wall in ("hrr", "grad", "bouzidi", "staircase"):
|
||||
for off in (0.0, 0.25, 0.5):
|
||||
d, nx, ny = 16, 320, 192
|
||||
st = int(conv_steps(d, 60) * scale)
|
||||
add(f"C{len([r for r in RUNS if r['group']=='C'])+1:02d}_sub_{wall}_{int(off*100):02d}",
|
||||
"C", f"Субклеточный сдвиг тела {off} клетки, стенка {wall}",
|
||||
"разброс Cd по сдвигам: чем субсеточнее модель, тем он меньше",
|
||||
cyl_args(d, 20, nx, ny, body_y=96 + off, wall=wall, pert_amp=0),
|
||||
nx * ny, st)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# D. Профили крыла
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
def foil_args(chord, alpha, re, nx, ny, naca="0012", **kw):
|
||||
a = {"--shape": "naca", "--naca": naca, "--size": chord,
|
||||
"--body-angle": alpha, "--nx": nx, "--ny": ny,
|
||||
"--body-x": nx // 4, "--re": re, "--refine": 1}
|
||||
a.update({"--" + k.replace("_", "-"): v for k, v in kw.items()})
|
||||
return flatten(a)
|
||||
|
||||
|
||||
def group_d(scale):
|
||||
# поляра: наклон Cl(alpha) и положение сваливания
|
||||
for al in (0, 4, 8, 12, 16):
|
||||
c, nx, ny = 128, 2048, 1024
|
||||
st = int(conv_steps(c, 400) * scale)
|
||||
add(f"D{len([r for r in RUNS if r['group']=='D'])+1:02d}_naca0012_a{al:02d}", "D",
|
||||
f"NACA 0012, α={al}°, Re=1000, хорда 128",
|
||||
"линейный участок Cl(α) и срыв; сравнение с низкорейнольдсовой литературой",
|
||||
foil_args(c, al, 1000, nx, ny), nx * ny, st,
|
||||
gif=f"naca0012_a{al:02d}.gif", nx=nx)
|
||||
|
||||
for re in (500, 2000, 10000):
|
||||
c, nx, ny = 128, 2048, 1024
|
||||
st = int(conv_steps(c, 400) * scale)
|
||||
add(f"D{len([r for r in RUNS if r['group']=='D'])+1:02d}_naca0012_re{re}", "D",
|
||||
f"NACA 0012, α=8°, Re={re}",
|
||||
"перестройка следа с ростом Re; проверка устойчивости на профиле",
|
||||
foil_args(c, 8, re, nx, ny), nx * ny, st,
|
||||
gif=f"naca0012_re{re}.gif", nx=nx)
|
||||
|
||||
for al in (0, 6, 12):
|
||||
c, nx, ny = 128, 2048, 1024
|
||||
st = int(conv_steps(c, 400) * scale)
|
||||
add(f"D{len([r for r in RUNS if r['group']=='D'])+1:02d}_naca4412_a{al:02d}", "D",
|
||||
f"NACA 4412 (с изгибом), α={al}°, Re=1000",
|
||||
"ненулевой Cl при α=0 — прямая проверка средней линии профиля",
|
||||
foil_args(c, al, 1000, nx, ny, naca="4412"), nx * ny, st,
|
||||
gif=f"naca4412_a{al:02d}.gif", nx=nx)
|
||||
|
||||
for c in (48, 96, 192):
|
||||
nx, ny = 16 * c, 8 * c
|
||||
st = int(conv_steps(c, 300) * scale)
|
||||
add(f"D{len([r for r in RUNS if r['group']=='D'])+1:02d}_naca_chord{c}", "D",
|
||||
f"NACA 0012, α=8°, сходимость по хорде {c}",
|
||||
"Cl и Cd сходятся по разрешению хорды",
|
||||
foil_args(c, 8, 1000, nx, ny), nx * ny, st, nx=nx)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# E. Сложная и множественная геометрия
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
def group_e(scale):
|
||||
# тандем: литература даёт переключение режимов около L/D 3.5–4
|
||||
for ld in (1.5, 3.0, 5.0):
|
||||
d, nx, ny = 48, 1440, 720
|
||||
gap = int(ld * d)
|
||||
bodies = f"cylinder:d={d},x=360,y=360; cylinder:d={d},x={360 + gap},y=360"
|
||||
st = int(conv_steps(d, 800) * scale)
|
||||
add(f"E{len([r for r in RUNS if r['group']=='E'])+1:02d}_tandem_ld{int(ld*10)}", "E",
|
||||
f"Тандем цилиндров, зазор L/D={ld}",
|
||||
"при малом зазоре у заднего тела ОТРИЦАТЕЛЬНОЕ сопротивление; переход около 3.5–4",
|
||||
["--bodies", bodies, "--nx", str(nx), "--ny", str(ny), "--re", "150", "--refine", "1"],
|
||||
nx * ny, st, gif=f"tandem_ld{int(ld*10)}.gif", nx=nx)
|
||||
|
||||
# решётка как пористая среда
|
||||
for step_d, tag in ((3, "sparse"), (2, "dense")):
|
||||
d, nx, ny = 24, 1440, 720
|
||||
bodies = "; ".join(
|
||||
f"cylinder:d={d},x={360 + i * step_d * d},y={360 + (j - 1.5) * step_d * d}"
|
||||
for i in range(4) for j in range(4))
|
||||
st = int(conv_steps(d, 600) * scale)
|
||||
add(f"E{len([r for r in RUNS if r['group']=='E'])+1:02d}_array_{tag}", "E",
|
||||
f"Решётка 4×4 цилиндров, шаг {step_d}D — пористая среда",
|
||||
"суммарное сопротивление и структура течения сквозь набор тел",
|
||||
["--bodies", bodies, "--nx", str(nx), "--ny", str(ny), "--re", "150", "--refine", "1"],
|
||||
nx * ny, st, gif=f"array_{tag}.gif", nx=nx)
|
||||
|
||||
# перфорированная пластина: набор коротких пластин со щелями
|
||||
for open_frac, tag in ((0.2, "open20"), (0.5, "open50")):
|
||||
nx, ny = 1200, 600
|
||||
seg, gap = 40, int(40 * open_frac / (1 - open_frac))
|
||||
bodies = "; ".join(
|
||||
f"plate:d={seg},t=0.12,a=90,x=300,y={60 + k * (seg + gap)}"
|
||||
for k in range(max(1, (ny - 120) // (seg + gap))))
|
||||
st = int(conv_steps(seg, 500) * scale)
|
||||
add(f"E{len([r for r in RUNS if r['group']=='E'])+1:02d}_perf_{tag}", "E",
|
||||
f"Перфорированная пластина, скважность {open_frac}",
|
||||
"струи в щелях и общее сопротивление; проверка множества тонких тел",
|
||||
["--bodies", bodies, "--nx", str(nx), "--ny", str(ny), "--re", "500", "--refine", "1"],
|
||||
nx * ny, st, gif=f"perf_{tag}.gif", nx=nx)
|
||||
|
||||
# многоэлементный профиль: основной + предкрылок
|
||||
nx, ny = 2048, 1024
|
||||
bodies = ("naca:naca=4412,d=200,a=6,x=560,y=512; "
|
||||
"naca:naca=2412,d=70,a=18,x=430,y=470")
|
||||
st = int(conv_steps(200, 300) * scale)
|
||||
add(f"E{len([r for r in RUNS if r['group']=='E'])+1:02d}_multielem", "E",
|
||||
"Многоэлементный профиль: основной 4412 плюс предкрылок",
|
||||
"щель между элементами и раздельные Cl/Cd по телам",
|
||||
["--bodies", bodies, "--nx", str(nx), "--ny", str(ny), "--re", "2000", "--refine", "1"],
|
||||
nx * ny, st, gif="multielem.gif", nx=nx)
|
||||
|
||||
# сверхтонкая пластина: предел субсеточности, тело тоньше клетки
|
||||
for t, tag in ((0.02, "t05"), (0.01, "t025")):
|
||||
nx, ny = 1200, 600
|
||||
st = int(conv_steps(50, 500) * scale)
|
||||
add(f"E{len([r for r in RUNS if r['group']=='E'])+1:02d}_thin_{tag}", "E",
|
||||
f"Сверхтонкая пластина, толщина {t*50:.2f} клетки",
|
||||
"предел субсеточной границы: тело тоньше клетки; ждём откаты на простой отскок",
|
||||
["--shape", "plate", "--size", "50", "--thickness", str(t), "--body-angle", "20",
|
||||
"--nx", str(nx), "--ny", str(ny), "--re", "500", "--refine", "1"],
|
||||
nx * ny, st, gif=f"thin_{tag}.gif", nx=nx)
|
||||
|
||||
# клин и зазубренная кромка — субклеточная деталь на многоугольнике
|
||||
nx, ny = 1200, 600
|
||||
st = int(conv_steps(60, 500) * scale)
|
||||
add(f"E{len([r for r in RUNS if r['group']=='E'])+1:02d}_wedge", "E",
|
||||
"Клин остриём против потока", "острая кромка: особая точка геометрии",
|
||||
["--shape", "polygon", "--poly=-30,-18;36,0;-30,18", "--size", "60",
|
||||
"--nx", str(nx), "--ny", str(ny), "--re", "500", "--refine", "1"],
|
||||
nx * ny, st, gif="wedge.gif", nx=nx)
|
||||
saw = ";".join(f"{-30 + 12 * k},{(-1) ** k * 6}" for k in range(6)) + ";30,-18;-30,-18"
|
||||
add(f"E{len([r for r in RUNS if r['group']=='E'])+1:02d}_serrated", "E",
|
||||
"Зазубренная задняя кромка", "субклеточные зубцы: как их видит граница",
|
||||
["--shape", "polygon", f"--poly={saw}", "--size", "60",
|
||||
"--nx", str(nx), "--ny", str(ny), "--re", "500", "--refine", "1"],
|
||||
nx * ny, st, gif="serrated.gif", nx=nx)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# F. Поле влияния и границы домена
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
def group_f(scale):
|
||||
d = 48
|
||||
for up in (2, 6, 16):
|
||||
nx, ny = int((up + 20) * d), 12 * d
|
||||
st = int(conv_steps(d, 600) * scale)
|
||||
add(f"F{len([r for r in RUNS if r['group']=='F'])+1:02d}_inlet{up}d", "F",
|
||||
f"Отступ до входа {up}D",
|
||||
"с какого отступа результат перестаёт зависеть от положения входа",
|
||||
cyl_args(d, 150, nx, ny, body_x=up * d), nx * ny, st)
|
||||
for down in (5, 15, 40):
|
||||
nx, ny = int((6 + down) * d), 12 * d
|
||||
st = int(conv_steps(d, 600) * scale)
|
||||
add(f"F{len([r for r in RUNS if r['group']=='F'])+1:02d}_outlet{down}d", "F",
|
||||
f"Отступ до выхода {down}D",
|
||||
"с какой длины следа выход перестаёт влиять на силы",
|
||||
cyl_args(d, 150, nx, ny, body_x=6 * d), nx * ny, st)
|
||||
for r in (1, 2, 3):
|
||||
d, nx, ny = 32, 640, 384
|
||||
st = int(conv_steps(d, 600) * scale)
|
||||
nodes = nx * ny
|
||||
if r > 1:
|
||||
ax, bx = int(nx // 5 - 1.5 * d), int(nx // 5 + 6.25 * d)
|
||||
ay, by = int(ny / 2 - 2 * d), int(ny / 2 + 2 * d)
|
||||
nodes += r * (r * (bx - ax) + 1) * (r * (by - ay) + 1)
|
||||
add(f"F{len([r_ for r_ in RUNS if r_['group']=='F'])+1:02d}_refine{r}", "F",
|
||||
f"Вложенный патч ×{r}",
|
||||
"вносит ли связка уровней систематику: Cd и St обязаны совпасть",
|
||||
cyl_args(d, 150, nx, ny, refine=r), nodes, st)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# G. Старт, акустика, время жизни
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
def group_g(scale):
|
||||
d, nx, ny = 48, 960, 576
|
||||
for re in (150, 2000, 20000):
|
||||
for init in ("uniform", "rest"):
|
||||
st = int(conv_steps(d, 300) * scale)
|
||||
add(f"G{len([r for r in RUNS if r['group']=='G'])+1:02d}_xt_re{re}_{init}", "G",
|
||||
f"x–t диаграмма, Re={re}, старт {init}",
|
||||
"откуда идёт стартовое возмущение и раскачивается ли продольная мода",
|
||||
cyl_args(d, re, nx, ny, init=init, sponge_len=0,
|
||||
xt=f"xt_re{re}_{init}.csv", xt_every=200),
|
||||
nx * ny, st, gif=f"xt_re{re}_{init}.gif", nx=nx, extra_out=("xt",))
|
||||
for sp in (0, 40):
|
||||
st = int(conv_steps(d, 400) * scale)
|
||||
add(f"G{len([r for r in RUNS if r['group']=='G'])+1:02d}_sponge{sp}", "G",
|
||||
f"Губка выхода {sp} столбцов при Re=5000",
|
||||
"губка — единственное, что реально ест продольную моду",
|
||||
cyl_args(d, 5000, nx, ny, sponge_len=sp), nx * ny, st)
|
||||
for sp in (0, 40):
|
||||
st = int(conv_steps(d, 400) * scale)
|
||||
add(f"G{len([r for r in RUNS if r['group']=='G'])+1:02d}_spongein{sp}", "G",
|
||||
f"Губка входа {sp} столбцов при Re=5000",
|
||||
"помогает ли гасить волну до отражения от входа",
|
||||
cyl_args(d, 5000, nx, ny, sponge_in=sp), nx * ny, st)
|
||||
for re in (10000, 50000, 200000):
|
||||
st = int(conv_steps(d, 200) * scale)
|
||||
add(f"G{len([r for r in RUNS if r['group']=='G'])+1:02d}_limit_re{re}", "G",
|
||||
f"Предел устойчивости, Re={re}",
|
||||
"где именно гибнет счёт; часть этих прогонов обязана развалиться",
|
||||
cyl_args(d, re, nx, ny), nx * ny, st)
|
||||
# тест на время жизни: один очень длинный прогон
|
||||
st = int(5_000_000 * scale)
|
||||
add(f"G{len([r for r in RUNS if r['group']=='G'])+1:02d}_lifetime", "G",
|
||||
"Тест на время жизни: 5·10⁶ шагов",
|
||||
"не уплывает ли ⟨ρ⟩ и не деградирует ли решение на очень длинной дистанции",
|
||||
cyl_args(d, 150, nx, ny), nx * ny, st, gif="lifetime.gif", nx=nx)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# H. Инварианты и точность
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
def group_h(scale):
|
||||
d, nx, ny = 32, 640, 384
|
||||
st = int(conv_steps(d, 400) * scale)
|
||||
add(f"H{len([r for r in RUNS if r['group']=='H'])+1:02d}_sym_cyl", "H",
|
||||
"Симметрия: цилиндр при α=0 без возмущения",
|
||||
"Cl и Cm обязаны остаться ≈0 — это внутреннее свойство схемы, не литература",
|
||||
cyl_args(d, 40, nx, ny, pert_amp=0), nx * ny, st)
|
||||
add(f"H{len([r for r in RUNS if r['group']=='H'])+1:02d}_sym_foil", "H",
|
||||
"Симметрия: NACA 0012 при α=0 без возмущения",
|
||||
"Cl ≈ 0 у симметричного профиля под нулевым углом",
|
||||
foil_args(64, 0, 500, 1024, 512, pert_amp=0), 1024 * 512,
|
||||
int(conv_steps(64, 300) * scale))
|
||||
for al, tag in ((8, "plus"), (-8, "minus")):
|
||||
add(f"H{len([r for r in RUNS if r['group']=='H'])+1:02d}_mirror_{tag}", "H",
|
||||
f"Зеркальность: NACA 0012 при α={al}°",
|
||||
"ряды обязаны зеркалиться: Cl меняет знак, Cd совпадает",
|
||||
foil_args(64, al, 500, 1024, 512), 1024 * 512,
|
||||
int(conv_steps(64, 300) * scale))
|
||||
for ul in (0.02, 0.05, 0.1):
|
||||
st2 = int(conv_steps(d, 400, ul) * scale)
|
||||
add(f"H{len([r for r in RUNS if r['group']=='H'])+1:02d}_mach{int(ul*100):02d}", "H",
|
||||
f"Зависимость от числа Маха, u_lat={ul}",
|
||||
"результат обязан сходиться при Ma→0; расхождение и есть сжимаемостная ошибка",
|
||||
cyl_args(d, 150, nx, ny, u_lat=ul), nx * ny, st2)
|
||||
# прямой замер расхождения f32 и f64 от длины прогона
|
||||
for bk in ("cpu", "gpu"):
|
||||
add(f"H{len([r for r in RUNS if r['group']=='H'])+1:02d}_prec_{bk}", "H",
|
||||
f"Точность: одинаковая постановка на {bk}",
|
||||
"расхождение f32 против f64 в зависимости от длины прогона",
|
||||
cyl_args(d, 150, nx, ny), nx * ny, int(conv_steps(d, 800) * scale), backend=bk)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# I. Сверхмелкие сетки по всем формам
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
def group_i(scale):
|
||||
shapes = [
|
||||
("cylinder", ["--shape", "cylinder"]),
|
||||
("square", ["--shape", "square"]),
|
||||
("diamond", ["--shape", "diamond"]),
|
||||
("ellipse", ["--shape", "ellipse", "--thickness", "0.35"]),
|
||||
("naca", ["--shape", "naca", "--naca", "0012", "--body-angle", "8"]),
|
||||
("triangle", ["--shape", "triangle"]),
|
||||
("plate", ["--shape", "plate", "--thickness", "0.08", "--body-angle", "25"]),
|
||||
("wedge", ["--shape", "polygon", "--poly=-128,-72;154,0;-128,72"]),
|
||||
]
|
||||
d, nx, ny = 256, 4096, 2048
|
||||
st = int(conv_steps(d, 300) * scale)
|
||||
for tag, extra in shapes:
|
||||
add(f"I{len([r for r in RUNS if r['group']=='I'])+1:02d}_fine_{tag}", "I",
|
||||
f"Сверхмелкая сетка 4096×2048, {tag}, D=256",
|
||||
"держится ли субсеточная граница на предельном разрешении для этой формы",
|
||||
extra + ["--size", str(d), "--nx", str(nx), "--ny", str(ny),
|
||||
"--body-x", "820", "--re", "1000", "--refine", "1"],
|
||||
nx * ny, st, gif=f"fine_{tag}.gif", nx=nx)
|
||||
bodies = ("cylinder:d=200,x=800,y=1024; cylinder:d=200,x=1400,y=1024; "
|
||||
"naca:naca=4412,d=300,a=10,x=2200,y=1024")
|
||||
add(f"I{len([r for r in RUNS if r['group']=='I'])+1:02d}_fine_multi", "I",
|
||||
"Сверхмелкая сетка, композиция из трёх тел",
|
||||
"взаимодействие следов на предельном разрешении",
|
||||
["--bodies", bodies, "--nx", str(nx), "--ny", str(ny), "--re", "1000", "--refine", "1"],
|
||||
nx * ny, st, gif="fine_multi.gif", nx=nx)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--scale", type=float, default=2.0,
|
||||
help="общий множитель длительности всех прогонов")
|
||||
ap.add_argument("--out", default=os.path.join(os.path.dirname(__file__), "scenarios.json"))
|
||||
args = ap.parse_args()
|
||||
|
||||
for g in (group_a, group_b, group_c, group_d, group_e, group_f, group_g, group_h, group_i):
|
||||
g(args.scale)
|
||||
|
||||
doc = {
|
||||
"meta": {
|
||||
"note": "Стоимость в обновлениях узлов — единственная переносимая между машинами "
|
||||
"мера. Часы драйвер получает, поделив её на фактические MLUPS, которые "
|
||||
"замеряет на месте (--calibrate).",
|
||||
"gif_policy": "вся длительность прогона, реальная скорость, 10 кадр/с; кадр "
|
||||
"прореживается до ширины не больше 1280 пикселей",
|
||||
"scale": args.scale,
|
||||
"assumed_gpu_mlups": 1200,
|
||||
"assumed_cpu_mlups": 22,
|
||||
},
|
||||
"runs": RUNS,
|
||||
}
|
||||
with open(args.out, "w", encoding="utf-8") as f:
|
||||
json.dump(doc, f, ensure_ascii=False, indent=1)
|
||||
|
||||
print(f"прогонов: {len(RUNS)} файл: {args.out}\n")
|
||||
print(f"{'гр':>3}{'прогонов':>10}{'обновлений узлов':>20}{'часов GPU':>12}{'часов CPU':>12}")
|
||||
tot_gpu = tot_cpu = 0.0
|
||||
for g in sorted({r["group"] for r in RUNS}):
|
||||
rs = [r for r in RUNS if r["group"] == g]
|
||||
cost = sum(r["cost"] for r in rs)
|
||||
hg = sum(r["cost"] / 1.2e9 / 3600 for r in rs if r["backend"] == "gpu")
|
||||
hc = sum(r["cost"] / 2.2e7 / 3600 for r in rs if r["backend"] == "cpu")
|
||||
tot_gpu += hg
|
||||
tot_cpu += hc
|
||||
print(f"{g:>3}{len(rs):>10}{cost:>20.3e}{hg:>12.1f}{hc:>12.1f}")
|
||||
print(f"{'—':>3}{len(RUNS):>10}{sum(r['cost'] for r in RUNS):>20.3e}"
|
||||
f"{tot_gpu:>12.1f}{tot_cpu:>12.1f}")
|
||||
print(f"\nвсего ≈ {tot_gpu + tot_cpu:.1f} ч при 1200 MLUPS на GPU и 22 на CPU")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
# Предполётная проверка кампании: каждый сценарий запускается на два шага.
|
||||
#
|
||||
# python preflight.py # проверить все
|
||||
# python preflight.py --group I # только группу
|
||||
#
|
||||
# Ловит опечатки в ключах, несовместимые сочетания и пределы железа ДО того, как кампания
|
||||
# уйдёт считать на девяносто часов. Не бесплатная привычка, а окупившаяся: именно так
|
||||
# выяснилось, что вся группа сверхмелких сеток падала на пределе GPU в 65535 рабочих групп
|
||||
# на измерение, а девять прогонов группы F передавали --body-x дважды.
|
||||
#
|
||||
# Физику проверка не трогает вовсе: два шага — это ровно «запустилось и не упало».
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
BIN = os.environ.get("KBC2D_BIN") or os.path.join(
|
||||
HERE, "..", "target", "release", "kbc2d" + (".exe" if os.name == "nt" else ""))
|
||||
|
||||
# Всё, что пишет файлы, выкидываем: проверяем запуск, а не вывод. Ключ идёт со значением,
|
||||
# поэтому следующий за ним аргумент тоже пропускается.
|
||||
DROP = {"--gif", "--xt", "--case-csv", "--gif-field", "--gif-downsample", "--gif-scale",
|
||||
"--gif-every", "--gif-fps", "--series-every"}
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--group", help="только эта группа, например I")
|
||||
ap.add_argument("--scenarios", default=os.path.join(HERE, "scenarios.json"))
|
||||
args = ap.parse_args()
|
||||
|
||||
runs = json.load(open(args.scenarios, encoding="utf-8"))["runs"]
|
||||
if args.group:
|
||||
runs = [r for r in runs if r["group"].upper() == args.group.upper()]
|
||||
if not runs:
|
||||
print("под выборку не попал ни один прогон")
|
||||
return 0
|
||||
if not os.path.exists(BIN):
|
||||
print(f"не найден бинарь решателя: {BIN}")
|
||||
return 1
|
||||
|
||||
bad = []
|
||||
for i, r in enumerate(runs, 1):
|
||||
argv, skip = [], False
|
||||
for a in r["args"]:
|
||||
if skip:
|
||||
skip = False
|
||||
continue
|
||||
if a in DROP:
|
||||
skip = True
|
||||
continue
|
||||
argv.append(a)
|
||||
if "--steps" in argv:
|
||||
argv[argv.index("--steps") + 1] = "2"
|
||||
else:
|
||||
argv += ["--steps", "2"]
|
||||
|
||||
p = subprocess.run([BIN] + argv + ["--verbose", "quiet", "--report-every", "0"],
|
||||
capture_output=True, cwd=HERE)
|
||||
ok = p.returncode == 0
|
||||
print("." if ok else "X", end="", flush=True)
|
||||
if not ok:
|
||||
tail = (p.stdout + p.stderr).decode("utf-8", "replace").strip().splitlines()
|
||||
bad.append((r["id"], tail[-1] if tail else "(без вывода)"))
|
||||
if i % 40 == 0:
|
||||
print(f" {i}/{len(runs)}", flush=True)
|
||||
|
||||
print(f"\n\nпроверено сценариев: {len(runs)}, не запустились: {len(bad)}")
|
||||
for id_, msg in bad:
|
||||
print(f" {id_:<28} {msg}")
|
||||
return 1 if bad else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,108 @@
|
||||
# Драйвер валидационной кампании для Windows. Делает ровно то же, что run_campaign.sh, и
|
||||
# читает тот же scenarios.json — целевая машина Linux, а этот вариант нужен для локальной
|
||||
# отладки обвязки.
|
||||
#
|
||||
# .\run_campaign.ps1 -DryRun
|
||||
# .\run_campaign.ps1 -Calibrate
|
||||
# .\run_campaign.ps1 -Smoke
|
||||
# .\run_campaign.ps1 -Resume -Group A,B
|
||||
# .\run_campaign.ps1 -Only cyl_re150 -BudgetHours 6
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$DryRun,
|
||||
[switch]$Resume,
|
||||
[switch]$Calibrate,
|
||||
[switch]$Smoke,
|
||||
[string[]]$Group,
|
||||
[string]$Only,
|
||||
[double]$BudgetHours = 0,
|
||||
[string]$Bin = "..\target\release\kbc2d.exe",
|
||||
[string]$Scenarios = "scenarios.json",
|
||||
[string]$Out = "out",
|
||||
[double]$GpuMlups = 1200,
|
||||
[double]$CpuMlups = 22
|
||||
)
|
||||
|
||||
Set-Location $PSScriptRoot
|
||||
if (-not (Test-Path $Bin)) { throw "не найден бинарь решателя: $Bin (собери cargo build --release)" }
|
||||
if (-not (Test-Path $Scenarios)) { throw "не найден список сценариев: $Scenarios" }
|
||||
|
||||
if ($Calibrate) {
|
||||
"Калибровка на этой машине (три коротких прогона)…"
|
||||
foreach ($c in @(@(240,120,'gpu'), @(960,480,'gpu'), @(480,240,'cpu'))) {
|
||||
$o = & $Bin --nx $c[0] --ny $c[1] --size 16 --refine 1 --steps 3000 `
|
||||
--report-every 3000 --verbose full --backend $c[2] 2>$null
|
||||
$m = ($o | Select-String 'MLUPS' | Select-Object -Last 1)
|
||||
" $($c[0])x$($c[1]) на $($c[2]): $(if ($m) { ($m -replace '.*?([0-9.]+) MLUPS.*','$1') } else { 'не измерено' })"
|
||||
}
|
||||
""
|
||||
"Подставь замеренное: .\run_campaign.ps1 -DryRun -GpuMlups <число> -CpuMlups <число>"
|
||||
return
|
||||
}
|
||||
|
||||
$runs = (Get-Content $Scenarios -Raw -Encoding UTF8 | ConvertFrom-Json).runs
|
||||
if ($Group) { $g = $Group | ForEach-Object { $_.ToUpper() }; $runs = $runs | Where-Object { $g -contains $_.group.ToUpper() } }
|
||||
if ($Only) { $runs = $runs | Where-Object { $_.id -like "*$Only*" } }
|
||||
if ($Smoke) { $runs = $runs | Sort-Object cost | Select-Object -First 3 }
|
||||
if (-not $runs) { "под выборку не попал ни один прогон"; return }
|
||||
|
||||
$totalCost = ($runs | Measure-Object -Property cost -Sum).Sum
|
||||
$estH = $totalCost / ($GpuMlups * 1e6) / 3600
|
||||
"прогонов: $($runs.Count) обновлений узлов: {0:e3} ≈ {1:F1} ч при $GpuMlups MLUPS" -f $totalCost, $estH
|
||||
"результаты: $Out\<id>\"
|
||||
""
|
||||
|
||||
if ($DryRun) {
|
||||
"{0,-28} {1,-3} {2,-4} {3,12} {4,8} {5}" -f 'id','гр','бэк','шагов','часов','название'
|
||||
foreach ($r in $runs) {
|
||||
$m = if ($r.backend -eq 'cpu') { $CpuMlups } else { $GpuMlups }
|
||||
"{0,-28} {1,-3} {2,-4} {3,12} {4,8:F2} {5}" -f $r.id, $r.group, $r.backend, $r.steps, ($r.cost/($m*1e6)/3600), $r.title
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force $Out | Out-Null
|
||||
$summary = Join-Path $Out 'summary.csv'
|
||||
$log = Join-Path $Out 'campaign.log'
|
||||
if (-not (Test-Path $summary)) { 'id,group,backend,status,seconds,steps,cost,title' | Out-File $summary -Encoding utf8 }
|
||||
|
||||
$started = Get-Date
|
||||
foreach ($r in $runs) {
|
||||
$dir = Join-Path $Out $r.id
|
||||
if ($Resume -and (Test-Path (Join-Path $dir 'summary.json'))) { "· $($r.id) — уже посчитан, пропускаю"; continue }
|
||||
if ($BudgetHours -gt 0 -and ((Get-Date) - $started).TotalHours -ge $BudgetHours) {
|
||||
"бюджет $BudgetHours ч исчерпан, останавливаюсь"; break
|
||||
}
|
||||
New-Item -ItemType Directory -Force $dir | Out-Null
|
||||
|
||||
# относительные имена файлов из сценария кладём внутрь папки прогона
|
||||
$argv = @()
|
||||
for ($i = 0; $i -lt $r.args.Count; $i++) {
|
||||
$v = $r.args[$i]
|
||||
if ($i -gt 0 -and @('--gif','--xt','--case-csv') -contains $r.args[$i-1]) { $v = Join-Path $dir $v }
|
||||
$argv += $v
|
||||
}
|
||||
$argv += @('--summary', (Join-Path $dir 'summary.json'),
|
||||
'--csv', (Join-Path $dir 'series.csv'),
|
||||
'--verbose','full','--report-every','5000')
|
||||
|
||||
"$Bin $($argv -join ' ')" | Out-File (Join-Path $dir 'cmd.txt') -Encoding utf8
|
||||
"▶ $($r.id) ($($r.group), $($r.backend)) $($r.title)"
|
||||
$t0 = Get-Date
|
||||
& $Bin @argv 2>&1 | Out-File (Join-Path $dir 'log.txt') -Encoding utf8
|
||||
$st = if ($LASTEXITCODE -eq 0) { 'ok' } else { 'fail' }
|
||||
$dt = [int]((Get-Date) - $t0).TotalSeconds
|
||||
if (Select-String -Path (Join-Path $dir 'log.txt') -Pattern 'РАЗВАЛИЛСЯ' -Quiet) { $st = 'blewup' }
|
||||
$rep = Get-Content (Join-Path $dir 'log.txt') -Encoding utf8
|
||||
$k = ($rep | Select-String 'ИТОГОВЫЙ ОТЧЁТ' | Select-Object -First 1).LineNumber
|
||||
if ($k) { $rep[($k-1)..($rep.Count-1)] | Out-File (Join-Path $dir 'report.txt') -Encoding utf8 }
|
||||
|
||||
'{0},{1},{2},{3},{4},{5},{6},"{7}"' -f $r.id,$r.group,$r.backend,$st,$dt,$r.steps,$r.cost,$r.title |
|
||||
Out-File $summary -Append -Encoding utf8
|
||||
"$(Get-Date -Format o) $($r.id) $st ${dt}s" | Out-File $log -Append -Encoding utf8
|
||||
" → $st за $dt с"
|
||||
}
|
||||
|
||||
""
|
||||
"готово. сводная таблица: $summary"
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env bash
|
||||
# Драйвер валидационной кампании. Читает scenarios.json, гоняет прогоны по одному, кладёт
|
||||
# логи, ряды, сводки и гифки каждого в собственную папку и ведёт общую таблицу.
|
||||
#
|
||||
# ./run_campaign.sh --dry-run смета: что и сколько будет считаться
|
||||
# ./run_campaign.sh --calibrate замерить фактические MLUPS этой машины
|
||||
# ./run_campaign.sh --smoke три коротких прогона: проверить обвязку
|
||||
# ./run_campaign.sh --resume считать, пропуская уже готовое
|
||||
# ./run_campaign.sh --group A,B только выбранные группы
|
||||
# ./run_campaign.sh --only cyl_re150 по подстроке идентификатора
|
||||
# ./run_campaign.sh --budget-hours 24 остановиться, когда время выйдет
|
||||
#
|
||||
# Прогон, который упал или развалился, помечается в сводке и НЕ останавливает кампанию:
|
||||
# группы E и G специально ищут предел устойчивости.
|
||||
|
||||
set -u
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
BIN="${KBC2D_BIN:-../target/release/kbc2d}"
|
||||
SCEN="${KBC2D_SCENARIOS:-scenarios.json}"
|
||||
OUT="${KBC2D_OUT:-out}"
|
||||
# ВНИМАНИЕ: имя GROUPS занято самим bash (список групп пользователя), присваивание в неё
|
||||
# молча игнорируется, а "$GROUPS" под root разворачивается в 0 — и выборка съедает всю
|
||||
# кампанию, не сказав ни слова. Отсюда GRP_SEL.
|
||||
DRY=0; RESUME=0; CALIB=0; SMOKE=0; GRP_SEL=""; ONLY=""; BUDGET=""
|
||||
GPU_MLUPS="${KBC2D_GPU_MLUPS:-1200}"
|
||||
CPU_MLUPS="${KBC2D_CPU_MLUPS:-22}"
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--dry-run) DRY=1 ;;
|
||||
--resume) RESUME=1 ;;
|
||||
--calibrate) CALIB=1 ;;
|
||||
--smoke) SMOKE=1 ;;
|
||||
--group) GRP_SEL="$2"; shift ;;
|
||||
--only) ONLY="$2"; shift ;;
|
||||
--budget-hours) BUDGET="$2"; shift ;;
|
||||
-h|--help) sed -n '2,20p' "$0"; exit 0 ;;
|
||||
*) echo "неизвестный ключ: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
command -v python3 >/dev/null 2>&1 && PY=python3 || PY=python
|
||||
[ -x "$BIN" ] || { echo "не найден бинарь решателя: $BIN (собери cargo build --release)" >&2; exit 1; }
|
||||
[ -f "$SCEN" ] || { echo "не найден список сценариев: $SCEN" >&2; exit 1; }
|
||||
|
||||
# ── калибровка: три коротких прогона на месте, чтобы оценки в часах были не гаданием ──
|
||||
if [ "$CALIB" = 1 ]; then
|
||||
echo "Калибровка на этой машине (три коротких прогона)…"
|
||||
for spec in "240 120 gpu" "960 480 gpu" "480 240 cpu"; do
|
||||
set -- $spec
|
||||
m=$("$BIN" --nx "$1" --ny "$2" --size 16 --refine 1 --steps 3000 --report-every 3000 \
|
||||
--verbose full --backend "$3" 2>/dev/null | grep -o '[0-9.]* MLUPS' | tail -1)
|
||||
echo " $1x$2 на $3: ${m:-не измерено}"
|
||||
done
|
||||
echo
|
||||
echo "Подставь замеренное в переменные окружения и запусти смету:"
|
||||
echo " KBC2D_GPU_MLUPS=<число> KBC2D_CPU_MLUPS=<число> ./run_campaign.sh --dry-run"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$OUT"
|
||||
SUMMARY="$OUT/summary.csv"
|
||||
LOG="$OUT/campaign.log"
|
||||
[ -f "$SUMMARY" ] || echo "id,group,backend,status,seconds,steps,cost,title" > "$SUMMARY"
|
||||
|
||||
# Выборку и порядок считает python: разбирать JSON башем — верный способ ошибиться.
|
||||
PLAN=$("$PY" - "$SCEN" "$GRP_SEL" "$ONLY" "$SMOKE" <<'PYEOF'
|
||||
import json, sys
|
||||
scen, groups, only, smoke = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4] == "1"
|
||||
runs = json.load(open(scen, encoding="utf-8"))["runs"]
|
||||
if groups:
|
||||
keep = {g.strip().upper() for g in groups.split(",")}
|
||||
runs = [r for r in runs if r["group"].upper() in keep]
|
||||
if only:
|
||||
runs = [r for r in runs if only in r["id"]]
|
||||
if smoke:
|
||||
# три самых дешёвых прогона: обвязку проверяем, а не физику
|
||||
runs = sorted(runs, key=lambda r: r["cost"])[:3]
|
||||
for r in runs:
|
||||
print("\t".join([r["id"], r["group"], r["backend"], str(r["cost"]),
|
||||
str(r["steps"]), r["title"], "\x1f".join(r["args"])]))
|
||||
PYEOF
|
||||
)
|
||||
|
||||
[ -n "$PLAN" ] || { echo "под выборку не попал ни один прогон"; exit 0; }
|
||||
|
||||
total_cost=0; n=0
|
||||
while IFS=$'\t' read -r id grp bk cost steps title args; do
|
||||
n=$((n + 1)); total_cost=$((total_cost + cost))
|
||||
done <<< "$PLAN"
|
||||
|
||||
est_h=$("$PY" -c "
|
||||
import sys
|
||||
print('%.1f' % (float(sys.argv[1]) / (float(sys.argv[2]) * 1e6) / 3600))" "$total_cost" "$GPU_MLUPS")
|
||||
|
||||
echo "прогонов: $n обновлений узлов: $total_cost ≈ $est_h ч при $GPU_MLUPS MLUPS"
|
||||
echo "результаты: $OUT/<id>/"
|
||||
echo
|
||||
|
||||
if [ "$DRY" = 1 ]; then
|
||||
printf '%-28s %-3s %-4s %12s %10s %s\n' id гр бэк шагов часов название
|
||||
while IFS=$'\t' read -r id grp bk cost steps title args; do
|
||||
m=$GPU_MLUPS; [ "$bk" = cpu ] && m=$CPU_MLUPS
|
||||
h=$("$PY" -c "import sys;print('%.2f'%(float(sys.argv[1])/(float(sys.argv[2])*1e6)/3600))" "$cost" "$m")
|
||||
printf '%-28s %-3s %-4s %12s %10s %s\n' "$id" "$grp" "$bk" "$steps" "$h" "$title"
|
||||
done <<< "$PLAN"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
started=$(date +%s)
|
||||
while IFS=$'\t' read -r id grp bk cost steps title args; do
|
||||
dir="$OUT/$id"
|
||||
if [ "$RESUME" = 1 ] && [ -f "$dir/summary.json" ]; then
|
||||
echo "· $id — уже посчитан, пропускаю"
|
||||
continue
|
||||
fi
|
||||
if [ -n "$BUDGET" ]; then
|
||||
spent=$(( ($(date +%s) - started) ))
|
||||
limit=$("$PY" -c "import sys;print(int(float(sys.argv[1])*3600))" "$BUDGET")
|
||||
[ "$spent" -ge "$limit" ] && { echo "бюджет $BUDGET ч исчерпан, останавливаюсь"; break; }
|
||||
fi
|
||||
|
||||
mkdir -p "$dir"
|
||||
# аргументы разделены символом 0x1f: в них есть пробелы (список тел) и запятые
|
||||
IFS=$'\x1f' read -r -a ARGV <<< "$args"
|
||||
ARGV+=(--summary "$dir/summary.json" --csv "$dir/series.csv" --verbose full --report-every 5000)
|
||||
# относительные имена файлов из сценария кладём внутрь папки прогона
|
||||
for i in "${!ARGV[@]}"; do
|
||||
case "${ARGV[$((i-1))]:-}" in
|
||||
--gif|--xt|--case-csv) ARGV[$i]="$dir/${ARGV[$i]}" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf '%s\n' "$BIN ${ARGV[*]}" > "$dir/cmd.txt"
|
||||
echo "▶ $id ($grp, $bk) $title"
|
||||
t0=$(date +%s)
|
||||
if "$BIN" "${ARGV[@]}" > "$dir/log.txt" 2>&1; then st=ok; else st=fail; fi
|
||||
t1=$(date +%s); dt=$((t1 - t0))
|
||||
grep -q "РАЗВАЛИЛСЯ" "$dir/log.txt" 2>/dev/null && st=blewup
|
||||
sed -n '/ИТОГОВЫЙ ОТЧЁТ/,$p' "$dir/log.txt" > "$dir/report.txt" 2>/dev/null
|
||||
|
||||
printf '%s,%s,%s,%s,%s,%s,%s,"%s"\n' "$id" "$grp" "$bk" "$st" "$dt" "$steps" "$cost" "$title" >> "$SUMMARY"
|
||||
echo "$(date -Is) $id $st ${dt}s" >> "$LOG"
|
||||
echo " → $st за ${dt} с"
|
||||
done <<< "$PLAN"
|
||||
|
||||
echo
|
||||
echo "готово. сводная таблица: $SUMMARY"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
# Запуск кампании на сервере с NVIDIA одной командой:
|
||||
# docker compose run --rm kbc2d --calibrate # сначала убедиться, что GPU виден
|
||||
# docker compose up -d # кампания в фоне
|
||||
# docker compose logs -f # смотреть ход
|
||||
#
|
||||
# Результаты складываются в ./out на хосте.
|
||||
|
||||
services:
|
||||
kbc2d:
|
||||
build: .
|
||||
image: kbc2d
|
||||
container_name: kbc2d
|
||||
volumes:
|
||||
- ./out:/work/bench/out
|
||||
environment:
|
||||
# Без graphics NVIDIA Container Toolkit не подложит Vulkan-ICD и wgpu не увидит карту.
|
||||
NVIDIA_DRIVER_CAPABILITIES: compute,utility,graphics
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
# Кампания идёт десятки часов и переживает перезапуск: --resume пропускает готовое.
|
||||
command: ["--resume"]
|
||||
restart: "no"
|
||||
Reference in New Issue
Block a user