Запуск в WSL2 через docker compose без NVIDIA-runtime
В WSL2 драйвера Vulkan для Linux у NVIDIA нет: карта отдаётся через /dev/dxg по протоколу WDDM, нативный libGLX_nvidia про него не знает и перечисляет ноль устройств, поэтому Container Toolkit нечего подкладывать внутрь и docker падает с `could not select device driver "nvidia"`. С /dev/dxg умеет говорить dzn (Dozen) — драйвер Mesa, транслирующий Vulkan в D3D12. Он положен в образ, и NVIDIA-runtime для этого пути не нужен вовсе: нужны проброс устройства и монтирование /usr/lib/wsl, где Microsoft держит libd3d12.so. Правка в решателе одна: флаги инстанса wgpu теперь читаются из окружения (InstanceFlags::from_build_config().with_env()). Без этого переменная WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER не действует, а wgpu по умолчанию МОЛЧА прячет адаптеры, не прошедшие тесты соответствия Vulkan, — под это правило попадает dzn, и решатель сообщал, что GPU не найден. Поведение по умолчанию не изменилось: без переменной такие адаптеры по-прежнему скрыты. База образа сменена с debian:bookworm-slim на archlinux:base: в пакетах Mesa у Debian и Ubuntu dzn не собирают (проверено по спискам файлов), в Arch он лежит отдельным пакетом той же версии Mesa, что и на хосте WSL. Новое: * docker-compose.wsl.yml — путь через /dev/dxg, с профилем проверок и с build: на случай, когда доступа к реестру нет; * bench/parity.py — сверка GPU-пути с CPU в f64 на течениях, где расхождение f32 и f64 не нарастает. Соответствие dzn вендором не проверено, значит проверяем сами, а не верим на слово. Замерено на Intel Iris Xe (та же карта, нативный драйвер против dzn): * точность: cd 2.39486 против 2.39486, energy_end 5.74813e-05 против 5.74810e-05 — совпадение до 5-6 значащих цифр; * скорость: плата за трансляцию падает с ростом сетки, 4.2x на 61 тыс. узлов, 1.46x на 461 тыс., 1.30x на 1.84 млн. На 95.1% стоимости кампании сетки крупнее 600 тыс. узлов, поэтому ожидаемое удорожание — около трети, не в разы. В калибровку добавлена сетка 1920x960: оценивать кампанию по 240x120 значит занижать пропускную способность вчетверо. Образ опубликован как notbigghost/kbc2d:1.1.0 (он же latest), проверен вытягиванием из реестра. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
|
||||
## Быстрый старт на сервере
|
||||
|
||||
Образ опубликован, собирать ничего не нужно: **`notbigghost/kbc2d:1.0.0`**. Исходники на
|
||||
Образ опубликован, собирать ничего не нужно: **`notbigghost/kbc2d:1.1.0`**. Исходники на
|
||||
сервере тоже не нужны — переносится один файл `docker-compose.server.yml`.
|
||||
|
||||
```sh
|
||||
@@ -40,6 +40,77 @@ KBC2D_GPU_MLUPS=1450 KBC2D_CPU_MLUPS=40 docker compose -f $C --profile check run
|
||||
docker run --rm --gpus all nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi
|
||||
```
|
||||
|
||||
## Запуск в WSL2
|
||||
|
||||
Отдельный путь, потому что в WSL2 **драйвера Vulkan для Linux у NVIDIA не существует**.
|
||||
Карта отдаётся через `/dev/dxg` по протоколу WDDM; нативный `libGLX_nvidia` про него не
|
||||
знает и возвращает ноль устройств — загрузчик его выбрасывает. Отсюда и `could not select
|
||||
device driver "nvidia"`: NVIDIA Container Toolkit тут не поможет, потому что подкладывать
|
||||
внутрь нечего.
|
||||
|
||||
Зато с `/dev/dxg` умеет говорить **dzn** (Dozen) — драйвер Mesa, транслирующий Vulkan в
|
||||
D3D12. Он лежит в образе. NVIDIA-runtime при этом **не нужен вовсе**: достаточно проброса
|
||||
устройства и монтирования `/usr/lib/wsl`, где Microsoft держит `libd3d12.so`.
|
||||
|
||||
```sh
|
||||
C=docker-compose.wsl.yml
|
||||
docker compose -f $C --profile check run --rm vulkan # 1. карта видна?
|
||||
docker compose -f $C --profile check run --rm parity # 2. считает ли она правильно?
|
||||
docker compose -f $C --profile check run --rm calibrate # 3. и с какой скоростью?
|
||||
docker compose -f $C --profile check run --rm preflight # 4. все сценарии стартуют?
|
||||
docker compose -f $C --profile check run --rm plan # 5. смета в часах
|
||||
docker compose -f $C up -d # 6. кампания
|
||||
```
|
||||
|
||||
Если образа нет ни локально, ни в реестре — `docker compose -f $C build`, доступ к Docker
|
||||
Hub не обязателен.
|
||||
|
||||
### Шаг parity обязателен
|
||||
|
||||
dzn сообщает о себе `conformanceVersion = 0.0.0.0`: набор тестов соответствия Vulkan он не
|
||||
проходил. wgpu по этой причине по умолчанию **прячет** такие адаптеры, и решатель сообщает,
|
||||
что GPU не найден. Согласие считать на непроверенном драйвере даётся явно — переменной
|
||||
`WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER=1`, она прописана в `docker-compose.wsl.yml`.
|
||||
|
||||
Раз соответствие не проверено вендором, его проверяем сами. `bench/parity.py` гоняет два
|
||||
коротких эталона на CPU в f64 и на GPU и сверяет числа: вихрь Тейлора–Грина (есть точное
|
||||
решение) и стационарное обтекание цилиндра при Re=20. Течения выбраны намеренно не
|
||||
хаотические — там расхождение f32 и f64 не нарастает, поэтому заметное различие означает
|
||||
проблему драйвера, а не разрядности.
|
||||
|
||||
Замерено на Intel Iris Xe: **dzn совпадает с нативным драйвером той же карты до 5–6
|
||||
значащих цифр** (`energy_end` 3.02277e-07 против 3.02283e-07, `cd` 2.39486 против 2.39486).
|
||||
Точность трансляция не портит. Но это замер на Intel; на своей карте прогоните сами — две
|
||||
минуты.
|
||||
|
||||
### Чего трансляция стоит по скорости
|
||||
|
||||
Плата есть, но она почти вся — накладные расходы на вызов, а не на счёт, и потому падает
|
||||
с ростом сетки. Замерено на Intel Iris Xe, один и тот же решатель:
|
||||
|
||||
| сетка | узлов | нативный Vulkan | dzn в контейнере | плата |
|
||||
|---|---|---|---|---|
|
||||
| 320×192 | 61 тыс. | 188.8 MLUPS | 44.5 MLUPS | **4.2×** |
|
||||
| 960×480 | 461 тыс. | 125.3 MLUPS | 86.0 MLUPS | **1.46×** |
|
||||
| 1920×960 | 1.84 млн | 128.7 MLUPS | 98.7 MLUPS | **1.30×** |
|
||||
|
||||
Каждый шаг решателя — несколько отправок в очередь; на мелкой сетке трансляция вызова
|
||||
стоит дороже самого счёта, на крупной размазывается. Для кампании это решающее
|
||||
обстоятельство, потому что дорогие прогоны в ней как раз крупные:
|
||||
|
||||
| узлов в сетке | прогонов | доля стоимости кампании |
|
||||
|---|---|---|
|
||||
| < 150 тыс. | 18 | 0.0% |
|
||||
| 150–600 тыс. | 47 | 4.9% |
|
||||
| > 600 тыс. | 50 | **95.1%** |
|
||||
|
||||
То есть 95% времени кампания проводит там, где плата 1.3–1.5×, и почти не бывает там, где
|
||||
она четырёхкратна. Ожидаемое удорожание по всей кампании — около трети: 90 часов
|
||||
превращаются примерно в 115–120, а не в 400.
|
||||
|
||||
Проверьте на своей карте: профиль `calibrate` меряет три сетки, и ориентироваться надо на
|
||||
**1920×960** — она представительна для кампании, а 240×120 показывает худший случай.
|
||||
|
||||
## Своя сборка образа
|
||||
|
||||
Если нужен образ из текущего состояния репозитория, а не опубликованный:
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Сверка GPU-пути с эталонным CPU-путём.
|
||||
|
||||
Зачем. На настоящем сервере GPU-путь идёт через драйвер NVIDIA. В WSL такого драйвера
|
||||
для Linux нет, и единственный доступный Vulkan — dzn (Dozen), транслирующий вызовы в
|
||||
D3D12. Он честно сообщает о себе `conformanceVersion = 0.0.0.0`, то есть набор тестов
|
||||
соответствия не проходил. Арифметика f32 в обоих API описана одним и тем же IEEE 754,
|
||||
но точность трансцендентных функций (exp, log, sqrt — а они в энтропийном равновесии
|
||||
на каждом узле) стандартами задаётся с допуском в несколько ULP, и допуски эти разные.
|
||||
Плюс энтропийный стабилизатор сравнивает знаменатель с относительным порогом GREL=1e-8:
|
||||
достаточно мелкого расхождения, чтобы решение «вырожден или нет» поменялось.
|
||||
|
||||
Поэтому пригодность dzn нельзя принимать на веру — её надо мерить. Скрипт гоняет два
|
||||
коротких эталона на CPU (f64) и на GPU и сверяет числа из summary.json.
|
||||
|
||||
Прогоны подобраны намеренно НЕ хаотические: вихрь Тейлора–Грина затухает гладко и имеет
|
||||
точное решение, обтекание цилиндра при Re=20 стационарно. В таких течениях расхождение
|
||||
f32 и f64 не нарастает, поэтому любое заметное различие означает проблему драйвера, а не
|
||||
разрядности. На развитой дорожке (Re >= 100) сверять бессмысленно: там разойдётся любая
|
||||
пара запусков с разной арифметикой.
|
||||
|
||||
Порог взят с запасом от факта: на нативном Vulkan разница Cd между CPU-f64 и GPU-f32
|
||||
измерена и составила 0.007%.
|
||||
|
||||
python3 parity.py # сверка
|
||||
python3 parity.py --tol 2e-3 # свой порог
|
||||
python3 parity.py --keep out/ # оставить сводки для разбора
|
||||
|
||||
Код возврата 1, если хоть одно поле вышло за порог или прогон развалился.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
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 ""))
|
||||
|
||||
# Поля сводки: "fields" сверяются с порогом, "report" только печатаются.
|
||||
#
|
||||
# * energy_end, enstrophy_end — интегралы поля, ловят расхождение самого счёта;
|
||||
# * cd — интегральная сила, накапливает ошибку по всей границе тела;
|
||||
# * gamma_mean и degenerate_frac — поведение энтропийного стабилизатора, самый чуткий
|
||||
# индикатор расхождений в exp/log;
|
||||
# * rho_mean — сохранение массы, обязано совпадать почти побитово.
|
||||
#
|
||||
# Пороги откалиброваны по замеру на НАТИВНОМ драйвере Vulkan (Intel Iris Xe, Windows):
|
||||
# там же, где f64 и f32 сравниваются в отсутствие какой-либо трансляции, отклонения
|
||||
# составили cd 1.7e-5, rho_mean 1.5e-6, gamma_mean 6.5e-4, energy_end 3.4e-3 (вихрь
|
||||
# доведён до затухания). Пороги ниже взяты с запасом к этим числам, но заметно ниже
|
||||
# того, что дал бы по-настоящему сломанный драйвер.
|
||||
#
|
||||
# Прогоны намеренно НЕ доводятся до глубокого затухания: когда энергия падает до 1e-7,
|
||||
# f32 упирается в собственный шум, и сверять становится нечего — это свойство
|
||||
# разрядности, а не драйвера.
|
||||
CASES = [
|
||||
{
|
||||
"name": "Тейлор-Грин 128x128, 500 шагов",
|
||||
"args": ["--case", "taylor-green", "--nx", "128", "--ny", "128",
|
||||
"--refine", "1", "--steps", "500", "--case-every", "100"],
|
||||
# taylor_green_error намеренно НЕ в списке сверяемых, только в отчётных: это
|
||||
# разность почти равных величин, и её относительное отклонение f32 от f64
|
||||
# достигает десятков процентов на любом, в том числе нативном, драйвере.
|
||||
# Замерено на нативном Vulkan: 0.0116 (f64) против 0.0218 (f32). Сверять по
|
||||
# ней бессмысленно, а видеть её полезно.
|
||||
"fields": {"energy_end": 1.0, "enstrophy_end": 1.0, "gamma_mean": 1.0},
|
||||
"report": ["taylor_green_error"],
|
||||
},
|
||||
{
|
||||
"name": "цилиндр Re=20, стационар, 8000 шагов",
|
||||
"args": ["--shape", "cylinder", "--size", "16", "--nx", "320", "--ny", "192",
|
||||
"--re", "20", "--refine", "1", "--steps", "8000"],
|
||||
"fields": {"cd": 1.0, "rho_mean": 0.05, "gamma_mean": 2.0,
|
||||
"degenerate_frac": 5.0},
|
||||
"report": ["strouhal"],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def run(backend, args, summary_path):
|
||||
"""Один прогон. Возвращает разобранную сводку либо строку с ошибкой."""
|
||||
cmd = [BIN] + args + ["--backend", backend, "--verbose", "quiet",
|
||||
"--report-every", "0", "--summary", summary_path]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True, errors="replace")
|
||||
if proc.returncode != 0:
|
||||
tail = (proc.stderr or proc.stdout or "").strip().splitlines()
|
||||
return None, "\n".join(tail[-6:]) or f"код возврата {proc.returncode}"
|
||||
try:
|
||||
with open(summary_path, encoding="utf-8") as f:
|
||||
return json.load(f), None
|
||||
except Exception as e:
|
||||
return None, f"сводка не читается: {e}"
|
||||
|
||||
|
||||
def deviation(ref, got):
|
||||
"""Относительное отклонение; для околонулевых величин — абсолютное."""
|
||||
if abs(ref) > 1e-12:
|
||||
return abs(got - ref) / abs(ref)
|
||||
return abs(got - ref)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Сверка GPU-пути с CPU-эталоном")
|
||||
ap.add_argument("--tol", type=float, default=1e-3,
|
||||
help="базовый относительный порог (по умолчанию 1e-3)")
|
||||
ap.add_argument("--keep", metavar="DIR", help="куда положить сводки прогонов")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.exists(BIN):
|
||||
print(f"не найден бинарь решателя: {BIN}")
|
||||
return 1
|
||||
|
||||
workdir = args.keep or tempfile.mkdtemp(prefix="kbc2d-parity-")
|
||||
os.makedirs(workdir, exist_ok=True)
|
||||
failures = []
|
||||
speeds = []
|
||||
|
||||
for case in CASES:
|
||||
print(f"\n=== {case['name']} ===")
|
||||
summaries = {}
|
||||
for backend in ("cpu", "gpu"):
|
||||
path = os.path.join(workdir, f"{backend}.json")
|
||||
data, err = run(backend, case["args"], path)
|
||||
if err:
|
||||
print(f" {backend}: ПРОГОН НЕ УДАЛСЯ\n {err}")
|
||||
failures.append(f"{case['name']}: {backend} не отработал")
|
||||
break
|
||||
if data.get("blew_up"):
|
||||
print(f" {backend}: счёт РАЗВАЛИЛСЯ")
|
||||
failures.append(f"{case['name']}: {backend} развалился")
|
||||
break
|
||||
summaries[backend] = data
|
||||
if len(summaries) != 2:
|
||||
continue
|
||||
|
||||
cpu, gpu = summaries["cpu"], summaries["gpu"]
|
||||
m_cpu, m_gpu = cpu.get("mlups", 0.0), gpu.get("mlups", 0.0)
|
||||
speeds.append((case["name"], m_cpu, m_gpu))
|
||||
ratio = f", ускорение x{m_gpu / m_cpu:.1f}" if m_cpu > 0 else ""
|
||||
print(f" скорость: CPU {m_cpu:.1f} MLUPS, GPU {m_gpu:.1f} MLUPS{ratio}")
|
||||
|
||||
print(f" {'поле':<22}{'CPU (f64)':>16}{'GPU':>16}{'откл.':>12} порог")
|
||||
for field in case.get("report", []):
|
||||
if field in cpu and field in gpu:
|
||||
ref, got = float(cpu[field]), float(gpu[field])
|
||||
print(f" {field:<22}{ref:>16.6g}{got:>16.6g}{deviation(ref, got):>11.2e}"
|
||||
f" (только к сведению)")
|
||||
for field, mult in case["fields"].items():
|
||||
if field not in cpu or field not in gpu:
|
||||
print(f" {field:<22}{'нет в сводке':>16}")
|
||||
continue
|
||||
ref, got = float(cpu[field]), float(gpu[field])
|
||||
dev = deviation(ref, got)
|
||||
tol = args.tol * mult
|
||||
mark = "" if dev <= tol else " <-- ВЫШЕ ПОРОГА"
|
||||
print(f" {field:<22}{ref:>16.6g}{got:>16.6g}{dev:>11.2e} {tol:.1e}{mark}")
|
||||
if dev > tol:
|
||||
failures.append(f"{case['name']}: {field} отклонилось на {dev:.2e} "
|
||||
f"при пороге {tol:.1e}")
|
||||
|
||||
print()
|
||||
if speeds:
|
||||
print("Скорость по прогонам:")
|
||||
for name, m_cpu, m_gpu in speeds:
|
||||
print(f" {name}: CPU {m_cpu:.1f} MLUPS, GPU {m_gpu:.1f} MLUPS")
|
||||
print()
|
||||
|
||||
if failures:
|
||||
print("СВЕРКА НЕ ПРОЙДЕНА:")
|
||||
for f in failures:
|
||||
print(f" * {f}")
|
||||
print("\nGPU-путь на этом драйвере доверия не заслуживает — кампанию на нём "
|
||||
"запускать нельзя.")
|
||||
else:
|
||||
print("Сверка пройдена: GPU считает то же, что CPU в двойной точности.")
|
||||
|
||||
if not args.keep:
|
||||
shutil.rmtree(workdir, ignore_errors=True)
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -30,7 +30,9 @@ if (-not (Test-Path $Scenarios)) { throw "не найден список сце
|
||||
|
||||
if ($Calibrate) {
|
||||
"Калибровка на этой машине (три коротких прогона)…"
|
||||
foreach ($c in @(@(240,120,'gpu'), @(960,480,'gpu'), @(480,240,'cpu'))) {
|
||||
# 1920x960 добавлена не для красоты: на 95% стоимости кампании сетки крупнее
|
||||
# 600 тыс. узлов, и оценивать по мелким - значит занижать пропускную способность.
|
||||
foreach ($c in @(@(240,120,'gpu'), @(960,480,'gpu'), @(1920,960,'gpu'), @(480,240,'cpu'))) {
|
||||
$o = & $Bin --nx $c[0] --ny $c[1] --size 16 --refine 1 --steps 3000 `
|
||||
--report-every 3000 --verbose full --backend $c[2] 2>$null
|
||||
$m = ($o | Select-String 'MLUPS' | Select-Object -Last 1)
|
||||
|
||||
@@ -48,7 +48,9 @@ command -v python3 >/dev/null 2>&1 && PY=python3 || PY=python
|
||||
# ── калибровка: три коротких прогона на месте, чтобы оценки в часах были не гаданием ──
|
||||
if [ "$CALIB" = 1 ]; then
|
||||
echo "Калибровка на этой машине (три коротких прогона)…"
|
||||
for spec in "240 120 gpu" "960 480 gpu" "480 240 cpu"; do
|
||||
# 1920x960 добавлена не для красоты: на 95% стоимости кампании сетки крупнее
|
||||
# 600 тыс. узлов, и оценивать по мелким — значит занижать пропускную способность.
|
||||
for spec in "240 120 gpu" "960 480 gpu" "1920 960 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)
|
||||
|
||||
Reference in New Issue
Block a user