Files
CFDManager/docs/theory/2d_solver/bench/preflight.py
T
NotBigGhostandClaude Opus 5 6344c7d205 Внятные сообщения preflight и предел dzn на размер привязки
preflight печатал последнюю строку вывода упавшего прогона, а у паники Rust
последняя строка — «note: run with RUST_BACKTRACE=1», то есть ноль сведений о
причине. Теперь из вывода достаётся собственное сообщение решателя, а для паники —
то, что стоит ПОСЛЕ строки «panicked at». Вместо

  A10_turb_n2048_kbc   note: run with `RUST_BACKTRACE=1` ...

печатается

  A10_turb_n2048_kbc
      wgpu error: Validation Error | In Device::create_bind_group, label = 'level'
      | Buffer binding 0 range 150994944 exceeds `max_*_buffer_binding_size` limit 134217728

Заодно задокументировано само ограничение. dzn объявляет
max_storage_buffer_binding_size = 128 МиБ против гигабайтов у нативных драйверов,
при том что max_buffer_size у него 2047 МиБ: держать большой буфер можно, показать
шейдеру одной привязкой — нет. Потолок выходит 3.73 млн узлов (около 1920x1920),
и 14 прогонов кампании из 115 в него не влезают. На эти 14 приходится 70.7%
стоимости, так что для кампании ограничение решающее.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 18:35:07 +03:00

114 lines
4.8 KiB
Python

#!/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"}
# Строки, которые ничего не объясняют и только вытесняют полезные.
NOISE = (
"note: run with `RUST_BACKTRACE",
"note: Some details are omitted",
"WARNING: dzn is not a conformant",
"stack backtrace",
"Caused by:",
)
def explain(output):
"""Достать из вывода упавшего прогона то, ради чего его вообще читают.
Раньше бралась последняя строка вывода — а у паники Rust последняя строка это
«note: run with RUST_BACKTRACE=1», то есть ровно ноль сведений о причине.
Полезное лежит либо в собственном сообщении решателя, либо на строке ПОСЛЕ
«panicked at»: там текст самой паники.
"""
out = [l.rstrip() for l in output.splitlines() if l.strip()]
out = [l for l in out if not any(n in l for n in NOISE)]
if not out:
return "(без вывода)"
for l in out:
if l.lstrip().startswith("ОШИБКА"):
return l.strip()
for i, l in enumerate(out):
if "panicked at" in l:
rest = [x.strip() for x in out[i + 1:]]
return " | ".join(rest[:4]) if rest else l.strip()
return out[-1].strip()
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:
out = (p.stdout + p.stderr).decode("utf-8", "replace")
bad.append((r["id"], explain(out)))
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_}")
print(f" {msg}")
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())