Кампания на пять равных долей: контейнер сразу считает свою часть
gen_scenarios.py раскладывает 115 прогонов по пяти долям (LPT, поправка на размер сетки по замерам dzn) — по ≈24 ч каждая; поле shard в scenarios.json. run_campaign.sh/.ps1 получили --shard. Новая точка входа образа entrypoint.sh: при KBC2D_SHARD сама выбирает путь к карте (dzn или NVIDIA), проверяет vulkaninfo и parity.py и запускает долю. Для чужих ПК под Windows/WSL2 — docker-compose.shards.yml и shard.bat. Образ notbigghost/kbc2d:1.3.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -70,10 +70,12 @@ ENV NVIDIA_DRIVER_CAPABILITIES=compute,utility,graphics
|
|||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
COPY --from=build /src/target/release/kbc2d /work/target/release/kbc2d
|
COPY --from=build /src/target/release/kbc2d /work/target/release/kbc2d
|
||||||
COPY bench /work/bench
|
COPY bench /work/bench
|
||||||
RUN chmod +x /work/bench/run_campaign.sh
|
RUN chmod +x /work/bench/run_campaign.sh /work/bench/entrypoint.sh
|
||||||
|
|
||||||
# Кампания идёт десятки часов, поэтому по умолчанию образ НЕ запускает её: случайный
|
# Кампания идёт десятки часов, поэтому по умолчанию образ НЕ запускает её: случайный
|
||||||
# `docker run` покажет смету и выйдет.
|
# `docker run` покажет смету и выйдет. Иначе — если задана доля (KBC2D_SHARD=1..5, так делает
|
||||||
|
# docker-compose.shards.yml): тогда entrypoint.sh сам проверяет карту, сверяет её с CPU и
|
||||||
|
# сразу начинает считать свою часть.
|
||||||
WORKDIR /work/bench
|
WORKDIR /work/bench
|
||||||
ENTRYPOINT ["./run_campaign.sh"]
|
ENTRYPOINT ["./entrypoint.sh"]
|
||||||
CMD ["--dry-run"]
|
CMD ["--dry-run"]
|
||||||
|
|||||||
@@ -497,7 +497,7 @@ python preflight.py # каждый сценарий старту
|
|||||||
|
|
||||||
## Развёртывание (Docker)
|
## Развёртывание (Docker)
|
||||||
|
|
||||||
Собранный образ опубликован: **`notbigghost/kbc2d:1.2.0`** (он же `latest`, платформа `linux/amd64`).
|
Собранный образ опубликован: **`notbigghost/kbc2d:1.3.0`** (он же `latest`, платформа `linux/amd64`).
|
||||||
Исходники на сервере не нужны — достаточно перенести туда один файл
|
Исходники на сервере не нужны — достаточно перенести туда один файл
|
||||||
`docker-compose.server.yml`:
|
`docker-compose.server.yml`:
|
||||||
|
|
||||||
@@ -523,7 +523,17 @@ docker build -t kbc2d docs/theory/2d_solver
|
|||||||
docker run --rm --gpus all kbc2d --calibrate
|
docker run --rm --gpus all kbc2d --calibrate
|
||||||
```
|
```
|
||||||
|
|
||||||
`ENTRYPOINT` — драйвер кампании, `CMD` по умолчанию `--dry-run`: случайный `docker run` покажет
|
### Пять долей на пяти машинах
|
||||||
|
|
||||||
|
Кампания разложена на **пять равных по времени долей** (≈24 ч каждая, поле `shard` в
|
||||||
|
`scenarios.json`), чтобы считать её одновременно на пяти чужих ПК с видеокартой под Windows
|
||||||
|
или WSL2. На машину переносятся два файла — `docker-compose.shards.yml` и `shard.bat` — и
|
||||||
|
выполняется `shard.bat 3` (или `docker compose -f docker-compose.shards.yml up -d shard3`).
|
||||||
|
Контейнер сам проверяет карту, сверяет её с CPU-эталоном и сразу считает свою долю. Подробно —
|
||||||
|
`bench/README.md`, раздел «Пять долей».
|
||||||
|
|
||||||
|
`ENTRYPOINT` — `bench/entrypoint.sh`. Без `KBC2D_SHARD` он просто передаёт аргументы драйверу
|
||||||
|
кампании, и `CMD` по умолчанию `--dry-run`: случайный `docker run` покажет
|
||||||
смету и выйдет, а не запустит сточасовую задачу. В серверном compose политика перезапуска —
|
смету и выйдет, а не запустит сточасовую задачу. В серверном compose политика перезапуска —
|
||||||
`on-failure`, а не `unless-stopped`: кампания завершается штатно с кодом 0, и «перезапускать
|
`on-failure`, а не `unless-stopped`: кампания завершается штатно с кодом 0, и «перезапускать
|
||||||
всегда» крутило бы контейнер вхолостую по кругу, тогда как падение или перезагрузку хоста
|
всегда» крутило бы контейнер вхолостую по кругу, тогда как падение или перезагрузку хоста
|
||||||
|
|||||||
@@ -6,9 +6,67 @@
|
|||||||
схемы. Каждый прогон кладёт в собственную папку `cmd.txt`, `log.txt`, `report.txt`, `series.csv`
|
схемы. Каждый прогон кладёт в собственную папку `cmd.txt`, `log.txt`, `report.txt`, `series.csv`
|
||||||
и `summary.json`; гифку пишут **59 прогонов из 115** — остальным `--gif` не передаётся.
|
и `summary.json`; гифку пишут **59 прогонов из 115** — остальным `--gif` не передаётся.
|
||||||
|
|
||||||
|
## Пять долей: чужие ПК под Windows / WSL2
|
||||||
|
|
||||||
|
Кампания разложена на **пять долей, равных по времени**: у каждого прогона в `scenarios.json`
|
||||||
|
есть поле `shard` (1…5). Каждая доля — отдельный контейнер, который при развёртывании **сразу
|
||||||
|
начинает считать**: ни сборки, ни ручной цепочки проверок, всё окружение (решатель, Vulkan-
|
||||||
|
загрузчик, dzn, Python) — внутри образа.
|
||||||
|
|
||||||
|
На машину переносятся два файла из корня решателя — `docker-compose.shards.yml` и `shard.bat`:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
shard.bat 3
|
||||||
|
```
|
||||||
|
|
||||||
|
или из любой оболочки:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose -f docker-compose.shards.yml up -d shard3
|
||||||
|
docker compose -f docker-compose.shards.yml logs -f shard3
|
||||||
|
```
|
||||||
|
|
||||||
|
При старте контейнер (`entrypoint.sh`) сам делает то, что раньше делалось профилями check:
|
||||||
|
|
||||||
|
1. выбирает путь к карте — есть `/dev/dxg`, значит WSL2 / Docker Desktop и dzn; нет —
|
||||||
|
нативный Linux с NVIDIA Container Toolkit;
|
||||||
|
2. проверяет `vulkaninfo`: нужен хотя бы один не программный адаптер;
|
||||||
|
3. гоняет `parity.py` (~2 мин) и только при совпадении с CPU-эталоном идёт дальше; после
|
||||||
|
успеха кладёт `out/parity_shardN.ok`, и перезапуск сверку не повторяет;
|
||||||
|
4. `run_campaign.sh --resume --shard N`.
|
||||||
|
|
||||||
|
Любой провал на шагах 1–3 останавливает контейнер с объяснением, что поправить на хосте, — а
|
||||||
|
не отдаёт кампанию считать впустую.
|
||||||
|
|
||||||
|
**В контейнер нельзя положить** драйвер видеокарты: он должен стоять в самой Windows (Vulkan
|
||||||
|
внутри контейнера — это dzn, транслирующий вызовы в D3D12 драйвера хоста). И нужен Docker
|
||||||
|
Desktop с бэкендом WSL2 (так по умолчанию) либо Docker внутри WSL2.
|
||||||
|
|
||||||
|
| доля | прогонов | ≈ часов |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | 22 | 24.0 |
|
||||||
|
| 2 | 23 | 24.0 |
|
||||||
|
| 3 | 23 | 24.0 |
|
||||||
|
| 4 | 23 | 24.0 |
|
||||||
|
| 5 | 24 | 24.0 |
|
||||||
|
|
||||||
|
Часы — оценка при 1200 MLUPS на крупной сетке **с поправкой на размер сетки**: на dzn мелкие
|
||||||
|
сетки считаются медленнее на узел, а сверхмелкие из группы I идут через раздельные привязки
|
||||||
|
(кривая — `DZN_MLUPS` в `gen_scenarios.py`, по замерам из раздела «Чего трансляция стоит по
|
||||||
|
скорости»). Раскладка жадная: самый долгий прогон — в самую лёгкую долю. Девять прогонов
|
||||||
|
группы I (по ≈8.7 ч) разошлись по два на долю в четырёх долях, пятая получила один и добрала остальным.
|
||||||
|
Равенство долей — равенство на **одинаковых** картах: на разных доли закончатся в разное время.
|
||||||
|
Абсолютные часы на конкретной карте — `--dry-run --shard N` после `--calibrate`.
|
||||||
|
|
||||||
|
Пересобрать раскладку под другое число машин: `python gen_scenarios.py --shards 3`.
|
||||||
|
|
||||||
|
**Сбор результатов.** Каждая доля пишет в `./out` рядом с compose-файлом: каталоги прогонов
|
||||||
|
`out/<id>/` (уникальны по id), свою сводку `summary_shardN.csv` и свой журнал
|
||||||
|
`campaign_shardN.log`. Собрать кампанию целиком — скопировать все пять `out/` в одну папку.
|
||||||
|
|
||||||
## Быстрый старт на сервере
|
## Быстрый старт на сервере
|
||||||
|
|
||||||
Образ опубликован, собирать ничего не нужно: **`notbigghost/kbc2d:1.2.0`**. Исходники на
|
Образ опубликован, собирать ничего не нужно: **`notbigghost/kbc2d:1.3.0`**. Исходники на
|
||||||
сервере тоже не нужны — переносится один файл `docker-compose.server.yml`.
|
сервере тоже не нужны — переносится один файл `docker-compose.server.yml`.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -186,6 +244,7 @@ KBC2D_GPU_MLUPS=1400 ./run_campaign.sh --dry-run
|
|||||||
| `--group A,B` | только выбранные группы |
|
| `--group A,B` | только выбранные группы |
|
||||||
| `--only cyl_re150` | по подстроке идентификатора |
|
| `--only cyl_re150` | по подстроке идентификатора |
|
||||||
| `--budget-hours 24` | остановиться, когда время выйдет |
|
| `--budget-hours 24` | остановиться, когда время выйдет |
|
||||||
|
| `--shard 3` | только доля 3 (поле `shard` в `scenarios.json`); то же — переменная `KBC2D_SHARD` |
|
||||||
|
|
||||||
Прогон, который упал или развалился, помечается в сводке и **не останавливает кампанию**:
|
Прогон, который упал или развалился, помечается в сводке и **не останавливает кампанию**:
|
||||||
группы E и G специально ищут предел устойчивости, там развал — ожидаемый результат.
|
группы E и G специально ищут предел устойчивости, там развал — ожидаемый результат.
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Точка входа образа.
|
||||||
|
#
|
||||||
|
# Без KBC2D_SHARD — ровно прежнее поведение: аргументы уходят в run_campaign.sh, а по
|
||||||
|
# умолчанию (CMD --dry-run) печатается смета. Профили check в compose-файлах работают как раньше.
|
||||||
|
#
|
||||||
|
# С KBC2D_SHARD=K контейнер — одна из долей кампании, и считать он начинает сам, без ручной
|
||||||
|
# цепочки vulkan → parity → plan. Перед стартом три шага, каждый обрывает запуск с понятным
|
||||||
|
# сообщением, а не даёт кампании уйти считать впустую:
|
||||||
|
# 1. путь к карте: /dev/dxg есть — значит WSL2 или Docker Desktop, и работает dzn;
|
||||||
|
# нет — настоящий Linux, Vulkan-ICD подкладывает NVIDIA Container Toolkit;
|
||||||
|
# 2. vulkaninfo должен увидеть хотя бы один НЕпрограммный адаптер;
|
||||||
|
# 3. parity.py — сверка GPU с CPU-f64 на двух эталонах (~2 мин). Один раз на каталог out:
|
||||||
|
# после успеха кладётся маркер, и перезапуск контейнера её не повторяет.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
[ -n "${KBC2D_SHARD:-}" ] || exec ./run_campaign.sh "$@"
|
||||||
|
|
||||||
|
K="$KBC2D_SHARD"
|
||||||
|
OUT="${KBC2D_OUT:-out}"
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
say() { echo "[доля $K] $*"; }
|
||||||
|
die() { echo; echo "[доля $K] ОСТАНОВЛЕНО: $*" >&2; exit 3; }
|
||||||
|
|
||||||
|
# ── 1. путь к карте ──────────────────────────────────────────────────────────
|
||||||
|
if [ -e /dev/dxg ]; then
|
||||||
|
say "найден /dev/dxg — WSL2 / Docker Desktop, Vulkan через dzn (Vulkan → D3D12)"
|
||||||
|
[ -d /usr/lib/wsl/lib ] || die "нет /usr/lib/wsl/lib: смонтируйте /usr/lib/wsl:/usr/lib/wsl:ro
|
||||||
|
(в docker-compose.shards.yml это уже прописано)"
|
||||||
|
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-/usr/lib/wsl/lib}"
|
||||||
|
# dzn сообщает conformanceVersion 0.0.0.0, и без согласия wgpu его прячет. Согласие
|
||||||
|
# оправдано только потому, что шаг 3 ниже проверяет числа.
|
||||||
|
export WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER="${WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER:-1}"
|
||||||
|
else
|
||||||
|
say "/dev/dxg нет — нативный Linux, Vulkan-драйвер NVIDIA от Container Toolkit"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 2. видна ли карта ────────────────────────────────────────────────────────
|
||||||
|
info=$(vulkaninfo --summary 2>&1)
|
||||||
|
gpus=$(printf '%s\n' "$info" | grep -E 'deviceType *=' | grep -vc 'PHYSICAL_DEVICE_TYPE_CPU')
|
||||||
|
if [ "$gpus" -lt 1 ]; then
|
||||||
|
printf '%s\n' "$info" | tail -20 >&2
|
||||||
|
die "Vulkan не видит ни одной видеокарты. Что проверить на хосте:
|
||||||
|
* Windows / WSL2: драйвер видеокарты под Windows установлен и свежий; Docker Desktop
|
||||||
|
работает на бэкенде WSL2; контейнеру проброшены /dev/dxg и /usr/lib/wsl.
|
||||||
|
* Linux: установлен nvidia-container-toolkit, контейнер запущен с GPU, и в
|
||||||
|
NVIDIA_DRIVER_CAPABILITIES есть graphics (сейчас: ${NVIDIA_DRIVER_CAPABILITIES:-не задано})."
|
||||||
|
fi
|
||||||
|
printf '%s\n' "$info" | grep -E 'deviceName *=|driverName *=|deviceType *=' | sed 's/^ */ /'
|
||||||
|
|
||||||
|
# ── 3. считает ли карта правильно ────────────────────────────────────────────
|
||||||
|
mark="$OUT/parity_shard$K.ok"
|
||||||
|
if [ -f "$mark" ]; then
|
||||||
|
say "сверка GPU с CPU уже пройдена ранее ($mark)"
|
||||||
|
else
|
||||||
|
say "сверка GPU с CPU-эталоном (parity.py, около двух минут)…"
|
||||||
|
python3 parity.py || die "GPU расходится с CPU-эталоном — результаты этой машины нечем
|
||||||
|
защищать. Вывод сверки выше. Повторить: удалить контейнер и поднять заново."
|
||||||
|
date -Is > "$mark"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── кампания ─────────────────────────────────────────────────────────────────
|
||||||
|
say "старт: run_campaign.sh --resume --shard $K"
|
||||||
|
exec ./run_campaign.sh --resume --shard "$K" "$@"
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
# получаются циклами, а стоимость каждого прогона считается тут же и суммируется. Запуск:
|
# получаются циклами, а стоимость каждого прогона считается тут же и суммируется. Запуск:
|
||||||
# python gen_scenarios.py # перезаписать scenarios.json и напечатать смету
|
# python gen_scenarios.py # перезаписать scenarios.json и напечатать смету
|
||||||
# python gen_scenarios.py --scale 2 # растянуть все длительности вдвое
|
# python gen_scenarios.py --scale 2 # растянуть все длительности вдвое
|
||||||
|
# python gen_scenarios.py --shards 5 # на сколько равных по времени долей делить кампанию
|
||||||
#
|
#
|
||||||
# Стоимость меряется в ОБНОВЛЕНИЯХ УЗЛОВ (nodes_per_step * steps) — единственная переносимая
|
# Стоимость меряется в ОБНОВЛЕНИЯХ УЗЛОВ (nodes_per_step * steps) — единственная переносимая
|
||||||
# между машинами мера. Часы драйвер получает из неё, поделив на фактические MLUPS, которые
|
# между машинами мера. Часы драйвер получает из неё, поделив на фактические MLUPS, которые
|
||||||
@@ -23,6 +24,18 @@ GIF_MAX_W = 1280 # предельная ширина кадра после
|
|||||||
GIF_MIN_W = 480 # ниже не опускаемся: нечитаемая гифка бесполезнее большой
|
GIF_MIN_W = 480 # ниже не опускаемся: нечитаемая гифка бесполезнее большой
|
||||||
GIF_PX_BUDGET = 1.5e9 # кадры×пиксели на гифку; при 0.103 байта на пиксель это ~150 МБ
|
GIF_PX_BUDGET = 1.5e9 # кадры×пиксели на гифку; при 0.103 байта на пиксель это ~150 МБ
|
||||||
|
|
||||||
|
# Доли кампании: прогоны раскладываются по N контейнерам так, чтобы те считали примерно
|
||||||
|
# одинаковое ВРЕМЯ, а не одинаковое число узлов. Время на узел зависит от размера сетки:
|
||||||
|
# на мелкой накладные расходы на отправку в очередь дороже самого счёта. Кривая — замер
|
||||||
|
# dzn в контейнере (Intel Iris Xe, bench/README.md, «Чего трансляция стоит по скорости»),
|
||||||
|
# потому что доли рассчитаны на чужие Windows/WSL-машины, где путь к карте — именно dzn.
|
||||||
|
# Важна только ФОРМА кривой: абсолют нормируется к ASSUMED_GPU_MLUPS.
|
||||||
|
ASSUMED_GPU_MLUPS = 1200
|
||||||
|
ASSUMED_CPU_MLUPS = 22
|
||||||
|
DZN_MLUPS = [(61_440, 44.5), (460_800, 86.0), (1_843_200, 98.7)]
|
||||||
|
SPLIT_NODES = 3_730_000 # выше — раздельные привязки популяций, у них своя скорость
|
||||||
|
SPLIT_MLUPS = 67.4 # замер на 2048×2048
|
||||||
|
|
||||||
|
|
||||||
def conv_steps(d_cells, n_conv, u_lat=U_LAT):
|
def conv_steps(d_cells, n_conv, u_lat=U_LAT):
|
||||||
"""Сколько шагов нужно на n_conv конвективных времён D/U."""
|
"""Сколько шагов нужно на n_conv конвективных времён D/U."""
|
||||||
@@ -472,15 +485,50 @@ def group_i(scale):
|
|||||||
nx * ny, st, gif="fine_multi.gif", nx=nx)
|
nx * ny, st, gif="fine_multi.gif", nx=nx)
|
||||||
|
|
||||||
|
|
||||||
|
def est_hours(r):
|
||||||
|
"""Оценка времени прогона в часах для раскладки по долям (см. DZN_MLUPS)."""
|
||||||
|
if r["backend"] == "cpu":
|
||||||
|
return r["cost"] / (ASSUMED_CPU_MLUPS * 1e6) / 3600
|
||||||
|
n = r["nodes_per_step"]
|
||||||
|
if n > SPLIT_NODES:
|
||||||
|
m = SPLIT_MLUPS
|
||||||
|
elif n <= DZN_MLUPS[0][0]:
|
||||||
|
m = DZN_MLUPS[0][1]
|
||||||
|
elif n >= DZN_MLUPS[-1][0]:
|
||||||
|
m = DZN_MLUPS[-1][1]
|
||||||
|
else:
|
||||||
|
for (n0, m0), (n1, m1) in zip(DZN_MLUPS, DZN_MLUPS[1:]):
|
||||||
|
if n <= n1:
|
||||||
|
t = math.log(n / n0) / math.log(n1 / n0)
|
||||||
|
m = m0 + t * (m1 - m0)
|
||||||
|
break
|
||||||
|
rel = m / DZN_MLUPS[-1][1]
|
||||||
|
return r["cost"] / (ASSUMED_GPU_MLUPS * 1e6 * rel) / 3600
|
||||||
|
|
||||||
|
|
||||||
|
def assign_shards(runs, n):
|
||||||
|
"""Разложить прогоны по n долям жадно (LPT): самый долгий — в самую лёгкую долю.
|
||||||
|
Порядок прогонов внутри доли остаётся порядком групп. Возвращает часы по долям."""
|
||||||
|
load = [0.0] * n
|
||||||
|
for r in sorted(runs, key=lambda r: (-est_hours(r), r["id"])):
|
||||||
|
k = min(range(n), key=lambda i: load[i])
|
||||||
|
r["shard"] = k + 1
|
||||||
|
load[k] += est_hours(r)
|
||||||
|
return load
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
ap = argparse.ArgumentParser()
|
ap = argparse.ArgumentParser()
|
||||||
ap.add_argument("--scale", type=float, default=2.0,
|
ap.add_argument("--scale", type=float, default=2.0,
|
||||||
help="общий множитель длительности всех прогонов")
|
help="общий множитель длительности всех прогонов")
|
||||||
|
ap.add_argument("--shards", type=int, default=5,
|
||||||
|
help="на сколько равных по времени долей делить кампанию")
|
||||||
ap.add_argument("--out", default=os.path.join(os.path.dirname(__file__), "scenarios.json"))
|
ap.add_argument("--out", default=os.path.join(os.path.dirname(__file__), "scenarios.json"))
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
for g in (group_a, group_b, group_c, group_d, group_e, group_f, group_g, group_h, group_i):
|
for g in (group_a, group_b, group_c, group_d, group_e, group_f, group_g, group_h, group_i):
|
||||||
g(args.scale)
|
g(args.scale)
|
||||||
|
shard_hours = assign_shards(RUNS, args.shards)
|
||||||
|
|
||||||
doc = {
|
doc = {
|
||||||
"meta": {
|
"meta": {
|
||||||
@@ -490,12 +538,14 @@ def main():
|
|||||||
"gif_policy": "вся длительность прогона, реальная скорость, 10 кадр/с; кадр "
|
"gif_policy": "вся длительность прогона, реальная скорость, 10 кадр/с; кадр "
|
||||||
"прореживается до ширины не больше 1280 пикселей",
|
"прореживается до ширины не больше 1280 пикселей",
|
||||||
"scale": args.scale,
|
"scale": args.scale,
|
||||||
"assumed_gpu_mlups": 1200,
|
"assumed_gpu_mlups": ASSUMED_GPU_MLUPS,
|
||||||
"assumed_cpu_mlups": 22,
|
"assumed_cpu_mlups": ASSUMED_CPU_MLUPS,
|
||||||
|
"shards": args.shards,
|
||||||
|
"shard_hours": [round(h, 2) for h in shard_hours],
|
||||||
},
|
},
|
||||||
"runs": RUNS,
|
"runs": RUNS,
|
||||||
}
|
}
|
||||||
with open(args.out, "w", encoding="utf-8") as f:
|
with open(args.out, "w", encoding="utf-8", newline="\n") as f:
|
||||||
json.dump(doc, f, ensure_ascii=False, indent=1)
|
json.dump(doc, f, ensure_ascii=False, indent=1)
|
||||||
|
|
||||||
print(f"прогонов: {len(RUNS)} файл: {args.out}\n")
|
print(f"прогонов: {len(RUNS)} файл: {args.out}\n")
|
||||||
@@ -513,6 +563,12 @@ def main():
|
|||||||
f"{tot_gpu:>12.1f}{tot_cpu:>12.1f}")
|
f"{tot_gpu:>12.1f}{tot_cpu:>12.1f}")
|
||||||
print(f"\nвсего ≈ {tot_gpu + tot_cpu:.1f} ч при 1200 MLUPS на GPU и 22 на CPU")
|
print(f"\nвсего ≈ {tot_gpu + tot_cpu:.1f} ч при 1200 MLUPS на GPU и 22 на CPU")
|
||||||
|
|
||||||
|
print("\nдоли (часы с поправкой на размер сетки, см. DZN_MLUPS):")
|
||||||
|
for k, h in enumerate(shard_hours, 1):
|
||||||
|
n = sum(1 for r in RUNS if r["shard"] == k)
|
||||||
|
print(f" доля {k}: {n:>3} прогонов ≈ {h:5.1f} ч")
|
||||||
|
print(f" разброс: {(max(shard_hours) / min(shard_hours) - 1) * 100:.1f} %")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
# .\run_campaign.ps1 -Smoke
|
# .\run_campaign.ps1 -Smoke
|
||||||
# .\run_campaign.ps1 -Resume -Group A,B
|
# .\run_campaign.ps1 -Resume -Group A,B
|
||||||
# .\run_campaign.ps1 -Only cyl_re150 -BudgetHours 6
|
# .\run_campaign.ps1 -Only cyl_re150 -BudgetHours 6
|
||||||
|
# .\run_campaign.ps1 -Resume -Shard 3
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
@@ -16,6 +17,7 @@ param(
|
|||||||
[switch]$Smoke,
|
[switch]$Smoke,
|
||||||
[string[]]$Group,
|
[string[]]$Group,
|
||||||
[string]$Only,
|
[string]$Only,
|
||||||
|
[int]$Shard = 0,
|
||||||
[double]$BudgetHours = 0,
|
[double]$BudgetHours = 0,
|
||||||
[string]$Bin = "..\target\release\kbc2d.exe",
|
[string]$Bin = "..\target\release\kbc2d.exe",
|
||||||
[string]$Scenarios = "scenarios.json",
|
[string]$Scenarios = "scenarios.json",
|
||||||
@@ -45,6 +47,7 @@ if ($Calibrate) {
|
|||||||
|
|
||||||
$runs = (Get-Content $Scenarios -Raw -Encoding UTF8 | ConvertFrom-Json).runs
|
$runs = (Get-Content $Scenarios -Raw -Encoding UTF8 | ConvertFrom-Json).runs
|
||||||
if ($Group) { $g = $Group | ForEach-Object { $_.ToUpper() }; $runs = $runs | Where-Object { $g -contains $_.group.ToUpper() } }
|
if ($Group) { $g = $Group | ForEach-Object { $_.ToUpper() }; $runs = $runs | Where-Object { $g -contains $_.group.ToUpper() } }
|
||||||
|
if ($Shard) { $runs = $runs | Where-Object { $_.shard -eq $Shard } }
|
||||||
if ($Only) { $runs = $runs | Where-Object { $_.id -like "*$Only*" } }
|
if ($Only) { $runs = $runs | Where-Object { $_.id -like "*$Only*" } }
|
||||||
if ($Smoke) { $runs = $runs | Sort-Object cost | Select-Object -First 3 }
|
if ($Smoke) { $runs = $runs | Sort-Object cost | Select-Object -First 3 }
|
||||||
if (-not $runs) { "под выборку не попал ни один прогон"; return }
|
if (-not $runs) { "под выборку не попал ни один прогон"; return }
|
||||||
@@ -67,6 +70,11 @@ if ($DryRun) {
|
|||||||
New-Item -ItemType Directory -Force $Out | Out-Null
|
New-Item -ItemType Directory -Force $Out | Out-Null
|
||||||
$summary = Join-Path $Out 'summary.csv'
|
$summary = Join-Path $Out 'summary.csv'
|
||||||
$log = Join-Path $Out 'campaign.log'
|
$log = Join-Path $Out 'campaign.log'
|
||||||
|
# у каждой доли своя сводка и свой журнал, как в run_campaign.sh
|
||||||
|
if ($Shard) {
|
||||||
|
$summary = Join-Path $Out "summary_shard$Shard.csv"
|
||||||
|
$log = Join-Path $Out "campaign_shard$Shard.log"
|
||||||
|
}
|
||||||
if (-not (Test-Path $summary)) { 'id,group,backend,status,seconds,steps,cost,title' | Out-File $summary -Encoding utf8 }
|
if (-not (Test-Path $summary)) { 'id,group,backend,status,seconds,steps,cost,title' | Out-File $summary -Encoding utf8 }
|
||||||
|
|
||||||
$started = Get-Date
|
$started = Get-Date
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
# ./run_campaign.sh --group A,B только выбранные группы
|
# ./run_campaign.sh --group A,B только выбранные группы
|
||||||
# ./run_campaign.sh --only cyl_re150 по подстроке идентификатора
|
# ./run_campaign.sh --only cyl_re150 по подстроке идентификатора
|
||||||
# ./run_campaign.sh --budget-hours 24 остановиться, когда время выйдет
|
# ./run_campaign.sh --budget-hours 24 остановиться, когда время выйдет
|
||||||
|
# ./run_campaign.sh --shard 3 только доля 3 из scenarios.json (или KBC2D_SHARD=3)
|
||||||
#
|
#
|
||||||
# Прогон, который упал или развалился, помечается в сводке и НЕ останавливает кампанию:
|
# Прогон, который упал или развалился, помечается в сводке и НЕ останавливает кампанию:
|
||||||
# группы E и G специально ищут предел устойчивости.
|
# группы E и G специально ищут предел устойчивости.
|
||||||
@@ -23,6 +24,7 @@ OUT="${KBC2D_OUT:-out}"
|
|||||||
# молча игнорируется, а "$GROUPS" под root разворачивается в 0 — и выборка съедает всю
|
# молча игнорируется, а "$GROUPS" под root разворачивается в 0 — и выборка съедает всю
|
||||||
# кампанию, не сказав ни слова. Отсюда GRP_SEL.
|
# кампанию, не сказав ни слова. Отсюда GRP_SEL.
|
||||||
DRY=0; RESUME=0; CALIB=0; SMOKE=0; GRP_SEL=""; ONLY=""; BUDGET=""
|
DRY=0; RESUME=0; CALIB=0; SMOKE=0; GRP_SEL=""; ONLY=""; BUDGET=""
|
||||||
|
SHARD="${KBC2D_SHARD:-}"
|
||||||
GPU_MLUPS="${KBC2D_GPU_MLUPS:-1200}"
|
GPU_MLUPS="${KBC2D_GPU_MLUPS:-1200}"
|
||||||
CPU_MLUPS="${KBC2D_CPU_MLUPS:-22}"
|
CPU_MLUPS="${KBC2D_CPU_MLUPS:-22}"
|
||||||
|
|
||||||
@@ -35,7 +37,8 @@ while [ $# -gt 0 ]; do
|
|||||||
--group) GRP_SEL="$2"; shift ;;
|
--group) GRP_SEL="$2"; shift ;;
|
||||||
--only) ONLY="$2"; shift ;;
|
--only) ONLY="$2"; shift ;;
|
||||||
--budget-hours) BUDGET="$2"; shift ;;
|
--budget-hours) BUDGET="$2"; shift ;;
|
||||||
-h|--help) sed -n '2,20p' "$0"; exit 0 ;;
|
--shard) SHARD="$2"; shift ;;
|
||||||
|
-h|--help) sed -n '2,21p' "$0"; exit 0 ;;
|
||||||
*) echo "неизвестный ключ: $1" >&2; exit 2 ;;
|
*) echo "неизвестный ключ: $1" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
shift
|
shift
|
||||||
@@ -65,13 +68,22 @@ fi
|
|||||||
mkdir -p "$OUT"
|
mkdir -p "$OUT"
|
||||||
SUMMARY="$OUT/summary.csv"
|
SUMMARY="$OUT/summary.csv"
|
||||||
LOG="$OUT/campaign.log"
|
LOG="$OUT/campaign.log"
|
||||||
|
# У каждой доли своя сводка и свой журнал: папки out/ с разных машин сливаются простым
|
||||||
|
# копированием (каталоги прогонов уникальны по id), а доли на одной машине не пишут в один файл.
|
||||||
|
if [ -n "$SHARD" ]; then
|
||||||
|
SUMMARY="$OUT/summary_shard$SHARD.csv"
|
||||||
|
LOG="$OUT/campaign_shard$SHARD.log"
|
||||||
|
fi
|
||||||
[ -f "$SUMMARY" ] || echo "id,group,backend,status,seconds,steps,cost,title" > "$SUMMARY"
|
[ -f "$SUMMARY" ] || echo "id,group,backend,status,seconds,steps,cost,title" > "$SUMMARY"
|
||||||
|
|
||||||
# Выборку и порядок считает python: разбирать JSON башем — верный способ ошибиться.
|
# Выборку и порядок считает python: разбирать JSON башем — верный способ ошибиться.
|
||||||
PLAN=$("$PY" - "$SCEN" "$GRP_SEL" "$ONLY" "$SMOKE" <<'PYEOF'
|
PLAN=$("$PY" - "$SCEN" "$GRP_SEL" "$ONLY" "$SMOKE" "$SHARD" <<'PYEOF'
|
||||||
import json, sys
|
import json, sys
|
||||||
scen, groups, only, smoke = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4] == "1"
|
scen, groups, only, smoke = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4] == "1"
|
||||||
|
shard = sys.argv[5]
|
||||||
runs = json.load(open(scen, encoding="utf-8"))["runs"]
|
runs = json.load(open(scen, encoding="utf-8"))["runs"]
|
||||||
|
if shard:
|
||||||
|
runs = [r for r in runs if str(r.get("shard")) == shard]
|
||||||
if groups:
|
if groups:
|
||||||
keep = {g.strip().upper() for g in groups.split(",")}
|
keep = {g.strip().upper() for g in groups.split(",")}
|
||||||
runs = [r for r in runs if r["group"].upper() in keep]
|
runs = [r for r in runs if r["group"].upper() in keep]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@ name: kbc2d
|
|||||||
|
|
||||||
# ── общая часть всех сервисов ────────────────────────────────────────────────
|
# ── общая часть всех сервисов ────────────────────────────────────────────────
|
||||||
x-kbc2d: &kbc2d
|
x-kbc2d: &kbc2d
|
||||||
image: notbigghost/kbc2d:1.2.0
|
image: notbigghost/kbc2d:1.3.0
|
||||||
pull_policy: missing
|
pull_policy: missing
|
||||||
volumes:
|
volumes:
|
||||||
- ./out:/work/bench/out
|
- ./out:/work/bench/out
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Кампания, разделённая на ПЯТЬ равных по времени долей — по одной на машину.
|
||||||
|
#
|
||||||
|
# Рассчитан на чужой ПК с видеокартой под Windows (Docker Desktop) или WSL2. Файл
|
||||||
|
# САМОДОСТАТОЧЕН: тянет готовый образ из реестра, исходники не нужны. На машину переносятся
|
||||||
|
# два файла — этот и shard.bat — и запускается одна команда:
|
||||||
|
#
|
||||||
|
# shard.bat 3 # Windows, двойной клик не годится: нужен номер
|
||||||
|
# docker compose -f docker-compose.shards.yml up -d shard3 # то же вручную, из любой оболочки
|
||||||
|
# docker compose -f docker-compose.shards.yml logs -f shard3 # смотреть ход
|
||||||
|
#
|
||||||
|
# Контейнер стартует и сразу считает: сам проверяет, видна ли карта, сверяет её с CPU-эталоном
|
||||||
|
# (parity.py, ~2 мин) и идёт по своей доле. Если что-то не так — останавливается с объяснением.
|
||||||
|
#
|
||||||
|
# Что ДОЛЖНО стоять на машине (в контейнер это положить нельзя):
|
||||||
|
# * драйвер видеокарты под Windows — Vulkan внутри контейнера транслируется в D3D12 (dzn);
|
||||||
|
# * Docker Desktop с бэкендом WSL2 (по умолчанию так и есть) либо Docker внутри WSL2.
|
||||||
|
#
|
||||||
|
# Доли посчитаны так, чтобы на ОДИНАКОВЫХ картах закончиться одновременно (≈24 ч каждая при
|
||||||
|
# 1200 MLUPS, с поправкой на размер сетки — см. bench/gen_scenarios.py). На разных картах
|
||||||
|
# закончатся в разное время. Результаты — в ./out рядом с этим файлом; собрать их с пяти
|
||||||
|
# машин = скопировать все out/ в одну папку (каталоги прогонов не пересекаются, у каждой доли
|
||||||
|
# своя сводка summary_shardN.csv).
|
||||||
|
#
|
||||||
|
# Перезапуск безопасен: --resume пропускает всё, у чего уже есть summary.json.
|
||||||
|
|
||||||
|
name: kbc2d
|
||||||
|
|
||||||
|
x-kbc2d: &kbc2d
|
||||||
|
image: notbigghost/kbc2d:1.3.0
|
||||||
|
# Если образа нет ни локально, ни в реестре — `docker compose -f docker-compose.shards.yml
|
||||||
|
# build` соберёт его из каталога с этим файлом (нужны исходники).
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
VERSION: "1.3.0"
|
||||||
|
pull_policy: missing
|
||||||
|
devices:
|
||||||
|
# сама видеокарта (WDDM); есть в любом WSL2, в том числе внутри Docker Desktop
|
||||||
|
- /dev/dxg:/dev/dxg
|
||||||
|
volumes:
|
||||||
|
- ./out:/work/bench/out
|
||||||
|
# libd3d12.so и libdxcore.so, которые нужны dzn; каталог наполняет сам WSL
|
||||||
|
- /usr/lib/wsl:/usr/lib/wsl:ro
|
||||||
|
# Доля задаётся переменной KBC2D_SHARD у каждого сервиса. Команда указана явно только затем,
|
||||||
|
# чтобы не унаследовать от образа CMD --dry-run.
|
||||||
|
command: ["--resume"]
|
||||||
|
# on-failure, а НЕ unless-stopped: доля завершается штатно с кодом 0, и «перезапускать
|
||||||
|
# всегда» гоняло бы контейнер по кругу. Падение (OOM, сбой драйвера) будет подхвачено.
|
||||||
|
restart: on-failure:5
|
||||||
|
stop_grace_period: 30s
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "50m"
|
||||||
|
max-file: "5"
|
||||||
|
|
||||||
|
services:
|
||||||
|
shard1:
|
||||||
|
<<: *kbc2d
|
||||||
|
container_name: kbc2d-shard1
|
||||||
|
environment: { KBC2D_SHARD: "1" }
|
||||||
|
shard2:
|
||||||
|
<<: *kbc2d
|
||||||
|
container_name: kbc2d-shard2
|
||||||
|
environment: { KBC2D_SHARD: "2" }
|
||||||
|
shard3:
|
||||||
|
<<: *kbc2d
|
||||||
|
container_name: kbc2d-shard3
|
||||||
|
environment: { KBC2D_SHARD: "3" }
|
||||||
|
shard4:
|
||||||
|
<<: *kbc2d
|
||||||
|
container_name: kbc2d-shard4
|
||||||
|
environment: { KBC2D_SHARD: "4" }
|
||||||
|
shard5:
|
||||||
|
<<: *kbc2d
|
||||||
|
container_name: kbc2d-shard5
|
||||||
|
environment: { KBC2D_SHARD: "5" }
|
||||||
@@ -32,14 +32,14 @@ name: kbc2d
|
|||||||
|
|
||||||
# ── общая часть всех сервисов ────────────────────────────────────────────────
|
# ── общая часть всех сервисов ────────────────────────────────────────────────
|
||||||
x-kbc2d: &kbc2d
|
x-kbc2d: &kbc2d
|
||||||
image: notbigghost/kbc2d:1.2.0
|
image: notbigghost/kbc2d:1.3.0
|
||||||
# Контекст сборки — каталог с этим файлом. Если образа нет ни локально, ни в реестре,
|
# Контекст сборки — каталог с этим файлом. Если образа нет ни локально, ни в реестре,
|
||||||
# достаточно `docker compose -f docker-compose.wsl.yml build`: доступ к Docker Hub
|
# достаточно `docker compose -f docker-compose.wsl.yml build`: доступ к Docker Hub
|
||||||
# для запуска не обязателен.
|
# для запуска не обязателен.
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
args:
|
args:
|
||||||
VERSION: "1.2.0"
|
VERSION: "1.3.0"
|
||||||
pull_policy: missing
|
pull_policy: missing
|
||||||
devices:
|
devices:
|
||||||
- /dev/dxg:/dev/dxg
|
- /dev/dxg:/dev/dxg
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
@echo off
|
||||||
|
rem Запуск одной доли кампании kbc2d на этой машине: shard.bat 3
|
||||||
|
rem Нужны: драйвер видеокарты и Docker Desktop (бэкенд WSL2). Рядом должен лежать
|
||||||
|
rem docker-compose.shards.yml. Результаты появятся в папке out рядом с этим файлом.
|
||||||
|
chcp 65001 >nul
|
||||||
|
setlocal
|
||||||
|
cd /d "%~dp0"
|
||||||
|
|
||||||
|
set "N=%~1"
|
||||||
|
if "%N%"=="" goto usage
|
||||||
|
if "%N%"=="1" goto ok
|
||||||
|
if "%N%"=="2" goto ok
|
||||||
|
if "%N%"=="3" goto ok
|
||||||
|
if "%N%"=="4" goto ok
|
||||||
|
if "%N%"=="5" goto ok
|
||||||
|
:usage
|
||||||
|
echo Использование: shard.bat ^<номер доли 1..5^>
|
||||||
|
echo shard.bat 3 поднять долю 3 и смотреть её журнал
|
||||||
|
echo Остановить: docker compose -f docker-compose.shards.yml stop
|
||||||
|
exit /b 2
|
||||||
|
|
||||||
|
:ok
|
||||||
|
if not exist docker-compose.shards.yml (
|
||||||
|
echo Рядом с shard.bat нет docker-compose.shards.yml.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
docker info >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo Docker не отвечает. Запустите Docker Desktop, дождитесь зелёного статуса и повторите.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
docker compose -f docker-compose.shards.yml up -d shard%N%
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo Доля %N% запущена и считает в фоне. Журнал ниже; Ctrl+C закрывает только журнал,
|
||||||
|
echo счёт продолжается. Результаты: %CD%\out
|
||||||
|
echo.
|
||||||
|
docker compose -f docker-compose.shards.yml logs -f shard%N%
|
||||||
Reference in New Issue
Block a user