Запуск в 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:
@@ -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 build -t kbc2d docs/theory/2d_solver
|
||||||
# docker run --rm --gpus all kbc2d --calibrate # проверить, что GPU виден
|
# docker compose -f docker-compose.wsl.yml --profile check run --rm vulkan
|
||||||
# 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` и убедиться, что карта видна.
|
|
||||||
|
|
||||||
# ── сборка ───────────────────────────────────────────────────────────────────
|
# ── сборка ───────────────────────────────────────────────────────────────────
|
||||||
# Версия тулчейна закреплена: та же, на которой решатель собирался и проверялся.
|
# Версия тулчейна закреплена: та же, на которой решатель собирался и проверялся.
|
||||||
@@ -18,19 +26,18 @@ WORKDIR /src
|
|||||||
|
|
||||||
# Сначала только манифесты — тогда слой с зависимостями переиспользуется, пока они не менялись.
|
# Сначала только манифесты — тогда слой с зависимостями переиспользуется, пока они не менялись.
|
||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml Cargo.lock ./
|
||||||
RUN mkdir src && echo 'fn main() {}' > src/main.rs \
|
RUN mkdir src && echo 'fn main() {}' > src/main.rs && cargo build --release --locked 2>/dev/null || true && rm -rf src
|
||||||
&& cargo build --release --locked 2>/dev/null || true \
|
|
||||||
&& rm -rf src
|
|
||||||
|
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
# touch нужен, чтобы cargo не спутал новые исходники с заглушкой из слоя выше
|
# touch нужен, чтобы cargo не спутал новые исходники с заглушкой из слоя выше
|
||||||
RUN touch src/main.rs && cargo build --release --locked
|
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 VERSION=dev
|
||||||
ARG REVISION=unknown
|
ARG REVISION=unknown
|
||||||
LABEL org.opencontainers.image.title="kbc2d"
|
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.revision="${REVISION}"
|
||||||
LABEL org.opencontainers.image.source="https://gitea.arseniev.info/NotBigGhost/TurbulenceCAD"
|
LABEL org.opencontainers.image.source="https://gitea.arseniev.info/NotBigGhost/TurbulenceCAD"
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
# Справка, руководства и переводы занимают в базе Arch заметно больше, чем сам
|
||||||
libvulkan1 vulkan-tools python3 ca-certificates \
|
# драйвер, а в контейнере не нужны никому. NoExtract прописывается ДО установки.
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
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_VISIBLE_DEVICES=all
|
||||||
ENV NVIDIA_DRIVER_CAPABILITIES=compute,utility,graphics
|
ENV NVIDIA_DRIVER_CAPABILITIES=compute,utility,graphics
|
||||||
|
|
||||||
|
|||||||
@@ -492,8 +492,7 @@ python preflight.py # каждый сценарий старту
|
|||||||
|
|
||||||
## Развёртывание (Docker)
|
## Развёртывание (Docker)
|
||||||
|
|
||||||
Собранный образ опубликован: **`notbigghost/kbc2d:1.0.0`** (он же `latest`, он же по хешу
|
Собранный образ опубликован: **`notbigghost/kbc2d:1.1.0`** (он же `latest`, платформа `linux/amd64`).
|
||||||
коммита `8d17bc8`; все три тега — один digest `sha256:932d881d…`, платформа `linux/amd64`).
|
|
||||||
Исходники на сервере не нужны — достаточно перенести туда один файл
|
Исходники на сервере не нужны — достаточно перенести туда один файл
|
||||||
`docker-compose.server.yml`:
|
`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`
|
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.
|
||||||
|
|
||||||
## Дальше
|
## Дальше
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## Быстрый старт на сервере
|
## Быстрый старт на сервере
|
||||||
|
|
||||||
Образ опубликован, собирать ничего не нужно: **`notbigghost/kbc2d:1.0.0`**. Исходники на
|
Образ опубликован, собирать ничего не нужно: **`notbigghost/kbc2d:1.1.0`**. Исходники на
|
||||||
сервере тоже не нужны — переносится один файл `docker-compose.server.yml`.
|
сервере тоже не нужны — переносится один файл `docker-compose.server.yml`.
|
||||||
|
|
||||||
```sh
|
```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
|
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) {
|
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 `
|
$o = & $Bin --nx $c[0] --ny $c[1] --size 16 --refine 1 --steps 3000 `
|
||||||
--report-every 3000 --verbose full --backend $c[2] 2>$null
|
--report-every 3000 --verbose full --backend $c[2] 2>$null
|
||||||
$m = ($o | Select-String 'MLUPS' | Select-Object -Last 1)
|
$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
|
if [ "$CALIB" = 1 ]; then
|
||||||
echo "Калибровка на этой машине (три коротких прогона)…"
|
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
|
set -- $spec
|
||||||
m=$("$BIN" --nx "$1" --ny "$2" --size 16 --refine 1 --steps 3000 --report-every 3000 \
|
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)
|
--verbose full --backend "$3" 2>/dev/null | grep -o '[0-9.]* MLUPS' | tail -1)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ name: kbc2d
|
|||||||
|
|
||||||
# ── общая часть всех сервисов ────────────────────────────────────────────────
|
# ── общая часть всех сервисов ────────────────────────────────────────────────
|
||||||
x-kbc2d: &kbc2d
|
x-kbc2d: &kbc2d
|
||||||
image: notbigghost/kbc2d:1.0.0
|
image: notbigghost/kbc2d:1.1.0
|
||||||
pull_policy: missing
|
pull_policy: missing
|
||||||
volumes:
|
volumes:
|
||||||
- ./out:/work/bench/out
|
- ./out:/work/bench/out
|
||||||
|
|||||||
@@ -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 при этом должен быть создан заранее и принадлежать вам).
|
||||||
@@ -1014,6 +1014,22 @@ impl Sim {
|
|||||||
pub fn new(spec: Spec) -> Result<Sim, String> {
|
pub fn new(spec: Spec) -> Result<Sim, String> {
|
||||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
||||||
backends: wgpu::Backends::PRIMARY,
|
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()
|
..Default::default()
|
||||||
});
|
});
|
||||||
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
|
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
|
||||||
|
|||||||
Reference in New Issue
Block a user