From 81985b169e347838b29bf10db840ab1efe56e0d8 Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Mon, 31 Aug 2026 17:15:53 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=BF=D1=83=D1=81=D0=BA=20=D0=B2?= =?UTF-8?q?=20WSL2=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20docker=20compose=20?= =?UTF-8?q?=D0=B1=D0=B5=D0=B7=20NVIDIA-runtime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В 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 --- docs/theory/2d_solver/Dockerfile | 60 ++++-- docs/theory/2d_solver/README.md | 55 ++++- docs/theory/2d_solver/bench/README.md | 73 ++++++- docs/theory/2d_solver/bench/parity.py | 188 ++++++++++++++++++ docs/theory/2d_solver/bench/run_campaign.ps1 | 4 +- docs/theory/2d_solver/bench/run_campaign.sh | 4 +- .../2d_solver/docker-compose.server.yml | 2 +- docs/theory/2d_solver/docker-compose.wsl.yml | 114 +++++++++++ docs/theory/2d_solver/src/gpu.rs | 16 ++ 9 files changed, 488 insertions(+), 28 deletions(-) create mode 100644 docs/theory/2d_solver/bench/parity.py create mode 100644 docs/theory/2d_solver/docker-compose.wsl.yml diff --git a/docs/theory/2d_solver/Dockerfile b/docs/theory/2d_solver/Dockerfile index 7eff2b5..117b8cc 100644 --- a/docs/theory/2d_solver/Dockerfile +++ b/docs/theory/2d_solver/Dockerfile @@ -1,15 +1,23 @@ -# Образ для развёртывания решателя и валидационной кампании на сервере с NVIDIA. +# Образ решателя и валидационной кампании. # -# Сборка и запуск: +# Поддерживает два способа добраться до видеокарты, и выбирать между ними не нужно — +# доступным окажется ровно один: +# +# 1. НАСТОЯЩИЙ LINUX-СЕРВЕР с драйвером NVIDIA. Vulkan-ICD подкладывает внутрь +# NVIDIA Container Toolkit, и только если в NVIDIA_DRIVER_CAPABILITIES есть +# `graphics` — с одним `compute` wgpu не увидит ни одного адаптера. Запуск через +# docker-compose.server.yml. +# +# 2. WSL2. Драйвер NVIDIA для Linux там отсутствует как класс: карта отдаётся через +# /dev/dxg по протоколу WDDM, а нативный libGLX_nvidia про него не знает и +# enumerate возвращает ноль устройств. Зато с /dev/dxg умеет говорить dzn (Dozen) — +# драйвер Mesa, транслирующий Vulkan в D3D12. Он в образе и лежит. NVIDIA Container +# Toolkit при этом НЕ НУЖЕН: нужен проброс устройства и монтирование /usr/lib/wsl, +# где Microsoft держит libd3d12.so. Запуск через docker-compose.wsl.yml. +# +# Сборка и быстрая проверка: # docker build -t kbc2d docs/theory/2d_solver -# docker run --rm --gpus all kbc2d --calibrate # проверить, что GPU виден -# docker run -d --gpus all -v "$PWD/out:/work/bench/out" --name kbc2d kbc2d --resume -# -# ГЛАВНАЯ ТОНКОСТЬ — Vulkan в контейнере. NVIDIA Container Toolkit подкладывает внутрь -# Vulkan-ICD (nvidia_icd.json) только если в NVIDIA_DRIVER_CAPABILITIES есть `graphics`. -# С одним `compute` wgpu не увидит НИ ОДНОГО адаптера и решатель откажется стартовать с -# --backend gpu. Поэтому capabilities прописаны в образе, а vulkan-tools положен внутрь, -# чтобы первым делом можно было выполнить `vulkaninfo` и убедиться, что карта видна. +# docker compose -f docker-compose.wsl.yml --profile check run --rm vulkan # ── сборка ─────────────────────────────────────────────────────────────────── # Версия тулчейна закреплена: та же, на которой решатель собирался и проверялся. @@ -18,19 +26,18 @@ WORKDIR /src # Сначала только манифесты — тогда слой с зависимостями переиспользуется, пока они не менялись. COPY Cargo.toml Cargo.lock ./ -RUN mkdir src && echo 'fn main() {}' > src/main.rs \ - && cargo build --release --locked 2>/dev/null || true \ - && rm -rf src +RUN mkdir src && echo 'fn main() {}' > src/main.rs && cargo build --release --locked 2>/dev/null || true && rm -rf src COPY src ./src # touch нужен, чтобы cargo не спутал новые исходники с заглушкой из слоя выше RUN touch src/main.rs && cargo build --release --locked # ── исполнение ─────────────────────────────────────────────────────────────── -FROM debian:bookworm-slim +# База Arch, а не debian-slim, по одной причине: dzn собирают не все дистрибутивы. +# В mesa-vulkan-drivers Debian и Ubuntu его нет (проверено по списку файлов пакета), +# в Arch он лежит отдельным пакетом vulkan-dzn той же версии Mesa, что и на хосте WSL. +FROM archlinux:base -# Версия и коммит приходят снаружи (см. bench/publish.sh): образ на сервере должен -# однозначно сопоставляться с состоянием репозитория, из которого собран. ARG VERSION=dev ARG REVISION=unknown LABEL org.opencontainers.image.title="kbc2d" @@ -39,11 +46,24 @@ LABEL org.opencontainers.image.version="${VERSION}" LABEL org.opencontainers.image.revision="${REVISION}" LABEL org.opencontainers.image.source="https://gitea.arseniev.info/NotBigGhost/TurbulenceCAD" -RUN apt-get update && apt-get install -y --no-install-recommends \ - libvulkan1 vulkan-tools python3 ca-certificates \ - && rm -rf /var/lib/apt/lists/* +# Справка, руководства и переводы занимают в базе Arch заметно больше, чем сам +# драйвер, а в контейнере не нужны никому. NoExtract прописывается ДО установки. +RUN { echo 'NoExtract = usr/share/man/*'; \ + echo 'NoExtract = usr/share/doc/*'; \ + echo 'NoExtract = usr/share/info/*'; \ + echo 'NoExtract = usr/share/locale/*'; \ + echo 'NoExtract = usr/share/i18n/*'; } >> /etc/pacman.conf \ + && pacman -Syu --noconfirm --needed \ + vulkan-icd-loader vulkan-dzn vulkan-tools python \ + && pacman -Scc --noconfirm \ + && rm -rf /var/cache/pacman/pkg/* /var/lib/pacman/sync/* \ + /usr/share/man /usr/share/doc /usr/share/info /usr/share/locale -# Без graphics в списке возможностей Vulkan-ICD внутрь не попадёт — см. шапку. +# Проверка на этапе сборки: без dzn образ бесполезен для WSL, и узнать об этом надо +# здесь, а не через час после старта кампании. +RUN ls /usr/share/vulkan/icd.d/ && ls /usr/share/vulkan/icd.d/dzn_icd*.json > /dev/null + +# Для пути 1 (настоящий сервер). На WSL эти переменные ни на что не влияют. ENV NVIDIA_VISIBLE_DEVICES=all ENV NVIDIA_DRIVER_CAPABILITIES=compute,utility,graphics diff --git a/docs/theory/2d_solver/README.md b/docs/theory/2d_solver/README.md index 6966edb..b93e907 100644 --- a/docs/theory/2d_solver/README.md +++ b/docs/theory/2d_solver/README.md @@ -492,8 +492,7 @@ python preflight.py # каждый сценарий старту ## Развёртывание (Docker) -Собранный образ опубликован: **`notbigghost/kbc2d:1.0.0`** (он же `latest`, он же по хешу -коммита `8d17bc8`; все три тега — один digest `sha256:932d881d…`, платформа `linux/amd64`). +Собранный образ опубликован: **`notbigghost/kbc2d:1.1.0`** (он же `latest`, платформа `linux/amd64`). Исходники на сервере не нужны — достаточно перенести туда один файл `docker-compose.server.yml`: @@ -534,8 +533,56 @@ Vulkan-ICD (`nvidia_icd.json`) только если в `NVIDIA_DRIVER_CAPABILIT Проверено локально: образ собирается, кампания внутри него проходит смоук с монтированием результатов на хост, физика совпадает с хостовой до последней цифры (ошибка Тейлора–Грина 9.829e-3 при N=64 и 2.401e-3 при N=128 — те же значения, что вне контейнера), а `--backend gpu` -без проброшенной карты отказывает явным сообщением, а не считает молча. GPU-путь в контейнере -проверяется только на машине с картой. +без проброшенной карты отказывает явным сообщением, а не считает молча. + +### WSL2 — отдельный путь + +В WSL2 всё вышеописанное не работает, и не из-за настроек. **Драйвера Vulkan для Linux у +NVIDIA там нет**: карта отдаётся через `/dev/dxg` по протоколу WDDM, нативный +`libGLX_nvidia` про него не знает и перечисляет ноль устройств. Container Toolkit +подкладывать внутрь нечего, отсюда `could not select device driver "nvidia"`. + +Работает другое: **dzn** (Dozen) — драйвер Mesa, транслирующий Vulkan в D3D12, он умеет +говорить с `/dev/dxg` напрямую. Он положен в образ (ради него база сменена с +`debian:bookworm-slim` на `archlinux:base`: в пакетах Mesa у Debian и Ubuntu dzn не +собирают). NVIDIA-runtime для этого пути не нужен вовсе — нужны проброс устройства и +монтирование `/usr/lib/wsl`. Запуск через `docker-compose.wsl.yml`, подробности в +[`bench/README.md`](bench/README.md). + +Две вещи, которые надо знать про этот путь. + +**wgpu по умолчанию прячет несоответствующие адаптеры.** dzn сообщает о себе +`conformanceVersion = 0.0.0.0`, и wgpu молча его отбрасывает — решатель докладывает, что +GPU не найден. Согласие даётся явно, переменной `WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER=1`; +чтобы она вообще читалась, в `gpu.rs` при создании инстанса стоит +`InstanceFlags::from_build_config().with_env()`. Поведение по умолчанию не изменилось: без +переменной несоответствующие адаптеры по-прежнему скрыты. + +**Точность трансляция не портит.** Замерено на Intel Iris Xe одним и тем же прогоном +(`bench/parity.py`, цилиндр Re=20, 8000 шагов): + +| | Cd | energy_end (Тейлор–Грин) | +|---|---|---| +| CPU, f64 | 2.39490 | 5.74754e-05 | +| нативный Vulkan, f32 | 2.39486 | 5.74813e-05 | +| dzn в контейнере, f32 | 2.39486 | 5.74810e-05 | + +Числа dzn и нативного драйвера сходятся до 5–6 значащих цифр, и разница между ними меньше, +чем между любым из них и f64. Считает трансляция то же самое. + +**Скорость она портит, но тем меньше, чем крупнее сетка** — плата почти вся приходится на +трансляцию вызова, а не счёта: + +| сетка | узлов | нативно | 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× | + +Для кампании это решает дело: 95.1% её стоимости приходится на сетки крупнее 600 тыс. +узлов, а на сетки мельче 150 тыс. — 0.0%. Ожидаемое удорожание всей кампании в контейнере — +около трети, а не в разы. Проверяется профилем `calibrate`, который меряет в том числе +1920×960. ## Дальше diff --git a/docs/theory/2d_solver/bench/README.md b/docs/theory/2d_solver/bench/README.md index 00720fa..0d195bc 100644 --- a/docs/theory/2d_solver/bench/README.md +++ b/docs/theory/2d_solver/bench/README.md @@ -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 показывает худший случай. + ## Своя сборка образа Если нужен образ из текущего состояния репозитория, а не опубликованный: diff --git a/docs/theory/2d_solver/bench/parity.py b/docs/theory/2d_solver/bench/parity.py new file mode 100644 index 0000000..83a2f58 --- /dev/null +++ b/docs/theory/2d_solver/bench/parity.py @@ -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()) diff --git a/docs/theory/2d_solver/bench/run_campaign.ps1 b/docs/theory/2d_solver/bench/run_campaign.ps1 index 8e0e28e..6eccc45 100644 --- a/docs/theory/2d_solver/bench/run_campaign.ps1 +++ b/docs/theory/2d_solver/bench/run_campaign.ps1 @@ -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) diff --git a/docs/theory/2d_solver/bench/run_campaign.sh b/docs/theory/2d_solver/bench/run_campaign.sh index 2157e6c..2c4e4d8 100644 --- a/docs/theory/2d_solver/bench/run_campaign.sh +++ b/docs/theory/2d_solver/bench/run_campaign.sh @@ -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) diff --git a/docs/theory/2d_solver/docker-compose.server.yml b/docs/theory/2d_solver/docker-compose.server.yml index f7eb91e..c6f85a5 100644 --- a/docs/theory/2d_solver/docker-compose.server.yml +++ b/docs/theory/2d_solver/docker-compose.server.yml @@ -22,7 +22,7 @@ name: kbc2d # ── общая часть всех сервисов ──────────────────────────────────────────────── x-kbc2d: &kbc2d - image: notbigghost/kbc2d:1.0.0 + image: notbigghost/kbc2d:1.1.0 pull_policy: missing volumes: - ./out:/work/bench/out diff --git a/docs/theory/2d_solver/docker-compose.wsl.yml b/docs/theory/2d_solver/docker-compose.wsl.yml new file mode 100644 index 0000000..cbc4df9 --- /dev/null +++ b/docs/theory/2d_solver/docker-compose.wsl.yml @@ -0,0 +1,114 @@ +# Запуск решателя и кампании в WSL2 — там, где драйвера NVIDIA для Linux не существует. +# +# ЧЕМ ЭТОТ ФАЙЛ ОТЛИЧАЕТСЯ ОТ docker-compose.server.yml. Тот рассчитан на настоящий +# Linux-сервер: просит у Docker устройство через драйвер `nvidia`, а Vulkan-ICD внутрь +# подкладывает NVIDIA Container Toolkit. В WSL2 этого сделать нельзя в принципе — +# у NVIDIA там нет Linux-библиотеки Vulkan, карта отдаётся по протоколу WDDM через +# /dev/dxg. Отсюда и ошибка `could not select device driver "nvidia"`. +# +# Здесь NVIDIA-runtime не запрашивается ВООБЩЕ. Нужно ровно две вещи: +# * проброс /dev/dxg — сама видеокарта; +# * монтирование /usr/lib/wsl — там Microsoft держит libd3d12.so и libdxcore.so. +# Дальше работает dzn (Dozen) из образа: драйвер Mesa, транслирующий Vulkan в D3D12. +# Ни nvidia-container-toolkit, ни nvidia-ctk, ни правки в daemon.json не требуются. +# +# ВАЖНО, ПРОЧТИТЕ ДО ЗАПУСКА КАМПАНИИ. dzn сообщает о себе conformanceVersion 0.0.0.0, +# то есть набор тестов соответствия Vulkan не проходил. Поэтому порядок такой: +# +# docker compose -f docker-compose.wsl.yml --profile check run --rm vulkan # карта видна? +# docker compose -f docker-compose.wsl.yml --profile check run --rm parity # числа те же? +# docker compose -f docker-compose.wsl.yml --profile check run --rm calibrate # скорость какая? +# docker compose -f docker-compose.wsl.yml --profile check run --rm plan +# docker compose -f docker-compose.wsl.yml up -d +# docker compose -f docker-compose.wsl.yml logs -f +# +# Шаг parity — не формальность. Он гоняет вихрь Тейлора-Грина (есть точное решение) и +# стационарное обтекание цилиндра на CPU в f64 и на GPU, и сверяет числа. Если он не +# прошёл, кампанию запускать нельзя: результаты будет нечем защищать. +# +# Результаты складываются в ./out на хосте. + +name: kbc2d + +# ── общая часть всех сервисов ──────────────────────────────────────────────── +x-kbc2d: &kbc2d + image: notbigghost/kbc2d:1.1.0 + # Контекст сборки — каталог с этим файлом. Если образа нет ни локально, ни в реестре, + # достаточно `docker compose -f docker-compose.wsl.yml build`: доступ к Docker Hub + # для запуска не обязателен. + build: + context: . + args: + VERSION: "1.1.0" + pull_policy: missing + devices: + - /dev/dxg:/dev/dxg + volumes: + - ./out:/work/bench/out + # Только на чтение: контейнеру нужны отсюда libd3d12.so, libd3d12core.so и + # libdxcore.so. Каталог наполняет сам WSL из C:\Windows\System32\lxss\lib. + - /usr/lib/wsl:/usr/lib/wsl:ro + environment: + # Без этого dzn не найдёт D3D12 и молча не даст ни одного адаптера. + LD_LIBRARY_PATH: /usr/lib/wsl/lib + # Осознанное согласие считать на драйвере, не прошедшем тесты соответствия Vulkan. + # Без этой переменной wgpu прячет dzn и решатель сообщает, что адаптер не найден. + # Прежде чем запускать кампанию, обязательно пройдите профиль parity. + WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER: "1" + # Оценки в часах драйвер считает из этих чисел. Умолчания взяты с другой машины — + # подставьте сюда то, что напечатает профиль calibrate. + KBC2D_GPU_MLUPS: ${KBC2D_GPU_MLUPS:-1200} + KBC2D_CPU_MLUPS: ${KBC2D_CPU_MLUPS:-22} + +services: + # ── сама кампания: единственный сервис, который поднимается по `up -d` ────── + campaign: + <<: *kbc2d + container_name: kbc2d-campaign + # --resume пропускает всё, у чего уже есть summary.json: перезапуск продолжает с + # места, а не начинает заново. + command: ["--resume"] + # on-failure, а НЕ unless-stopped: кампания завершается штатно с кодом 0, и политика + # «перезапускать всегда» после её окончания крутила бы контейнер вхолостую. + restart: on-failure:5 + stop_grace_period: 30s + logging: + driver: json-file + options: + max-size: "50m" + max-file: "5" + + # ── проверки перед запуском (профиль check, сами не поднимаются) ──────────── + vulkan: + <<: *kbc2d + profiles: ["check"] + entrypoint: ["vulkaninfo"] + command: ["--summary"] + + parity: + <<: *kbc2d + profiles: ["check"] + entrypoint: ["python3"] + command: ["parity.py"] + + preflight: + <<: *kbc2d + profiles: ["check"] + entrypoint: ["python3"] + command: ["preflight.py"] + + calibrate: + <<: *kbc2d + profiles: ["check"] + command: ["--calibrate"] + + plan: + <<: *kbc2d + profiles: ["check"] + command: ["--dry-run"] +# ── если результаты нужны не под root ──────────────────────────────────────── +# Контейнер работает от root, поэтому файлы в ./out окажутся root:root. Чтобы они +# принадлежали вам, добавьте в x-kbc2d строку +# user: "${UID}:${GID}" +# и запускайте как UID=$(id -u) GID=$(id -g) docker compose -f ... up -d +# (каталог ./out при этом должен быть создан заранее и принадлежать вам). diff --git a/docs/theory/2d_solver/src/gpu.rs b/docs/theory/2d_solver/src/gpu.rs index 12c0682..ac7541f 100644 --- a/docs/theory/2d_solver/src/gpu.rs +++ b/docs/theory/2d_solver/src/gpu.rs @@ -1014,6 +1014,22 @@ impl Sim { pub fn new(spec: Spec) -> Result { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends: wgpu::Backends::PRIMARY, + // Флаги читаются из окружения, и нужно это ровно ради одного — + // WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER. + // + // По умолчанию wgpu ПРЯЧЕТ адаптеры, не прошедшие набор тестов соответствия + // Vulkan («Adapter is not Vulkan compliant, hiding adapter»), и делает это + // молча: request_adapter просто возвращает None. Под это правило попадает + // dzn (Dozen) — драйвер Mesa, транслирующий Vulkan в D3D12. В WSL2 он + // единственный способ добраться до карты: Linux-драйвера Vulkan у NVIDIA + // там нет, устройство отдаётся через /dev/dxg по протоколу WDDM. + // + // Поведение по умолчанию НЕ меняется: без переменной окружения + // несоответствующие адаптеры по-прежнему скрыты. Переменная — осознанное + // согласие считать на непроверенном драйвере, и прежде чем на нём считать, + // надо прогнать bench/parity.py: он сверяет GPU-путь с CPU в двойной + // точности на течениях с точным решением. + flags: wgpu::InstanceFlags::from_build_config().with_env(), ..Default::default() }); let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {