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