#!/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//" 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"