Кампания на пять равных долей: контейнер сразу считает свою часть
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:
@@ -5,6 +5,7 @@
|
||||
# получаются циклами, а стоимость каждого прогона считается тут же и суммируется. Запуск:
|
||||
# python gen_scenarios.py # перезаписать scenarios.json и напечатать смету
|
||||
# python gen_scenarios.py --scale 2 # растянуть все длительности вдвое
|
||||
# python gen_scenarios.py --shards 5 # на сколько равных по времени долей делить кампанию
|
||||
#
|
||||
# Стоимость меряется в ОБНОВЛЕНИЯХ УЗЛОВ (nodes_per_step * steps) — единственная переносимая
|
||||
# между машинами мера. Часы драйвер получает из неё, поделив на фактические MLUPS, которые
|
||||
@@ -23,6 +24,18 @@ GIF_MAX_W = 1280 # предельная ширина кадра после
|
||||
GIF_MIN_W = 480 # ниже не опускаемся: нечитаемая гифка бесполезнее большой
|
||||
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):
|
||||
"""Сколько шагов нужно на n_conv конвективных времён D/U."""
|
||||
@@ -472,15 +485,50 @@ def group_i(scale):
|
||||
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():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--scale", type=float, default=2.0,
|
||||
help="общий множитель длительности всех прогонов")
|
||||
ap.add_argument("--shards", type=int, default=5,
|
||||
help="на сколько равных по времени долей делить кампанию")
|
||||
ap.add_argument("--out", default=os.path.join(os.path.dirname(__file__), "scenarios.json"))
|
||||
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):
|
||||
g(args.scale)
|
||||
shard_hours = assign_shards(RUNS, args.shards)
|
||||
|
||||
doc = {
|
||||
"meta": {
|
||||
@@ -490,12 +538,14 @@ def main():
|
||||
"gif_policy": "вся длительность прогона, реальная скорость, 10 кадр/с; кадр "
|
||||
"прореживается до ширины не больше 1280 пикселей",
|
||||
"scale": args.scale,
|
||||
"assumed_gpu_mlups": 1200,
|
||||
"assumed_cpu_mlups": 22,
|
||||
"assumed_gpu_mlups": ASSUMED_GPU_MLUPS,
|
||||
"assumed_cpu_mlups": ASSUMED_CPU_MLUPS,
|
||||
"shards": args.shards,
|
||||
"shard_hours": [round(h, 2) for h in shard_hours],
|
||||
},
|
||||
"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)
|
||||
|
||||
print(f"прогонов: {len(RUNS)} файл: {args.out}\n")
|
||||
@@ -513,6 +563,12 @@ def main():
|
||||
f"{tot_gpu:>12.1f}{tot_cpu:>12.1f}")
|
||||
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__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user