Запуск в 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 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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
## Дальше
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user