Валидационная кампания на 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:
2026-08-15 17:36:40 +03:00
co-authored by Claude Opus 5
parent 26a8475396
commit 8d17bc8014
11 changed files with 6114 additions and 39 deletions
+150
View File
@@ -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"