Валидационная кампания на 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:
@@ -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())
|
||||
Reference in New Issue
Block a user