Раздельные привязки популяций: снят предел dzn на размер сетки

Массив популяций занимает nx*ny*9*4 байта и показывался шейдеру одной привязкой.
dzn объявляет max_storage_buffer_binding_size = 128 МиБ, поэтому потолок выходил
3.73 млн узлов: 14 прогонов кампании из 115 падали на создании bind group, а на
них приходится 70.7% её стоимости.

Ограничена при этом ровно привязка: max_buffer_size у dzn 2047 МиБ. Поэтому тот
же буфер теперь показывается девятью привязками, по одному направлению в каждой,
и потолок поднимается до 33.5 млн узлов — самая крупная сетка кампании
(4096x4096, 16.8 млн) проходит с запасом.

Как устроено:
 * шаг между направлениями выровнен на 256 байт (dir_stride), потому что смещение
   привязки обязано быть кратно min_storage_buffer_offset_alignment;
 * обращения к популяциям в WGSL идут через fget/fset/pget/pset, а их тело
   генерируется под вариант (build_shader);
 * вариант выбирается по max_storage_buffer_binding_size адаптера — где предела
   нет, собирается прежний общий, без switch в аксессорах;
 * KBC2D_SPLIT_POPULATIONS=1 включает раздельные принудительно: иначе сверить два
   варианта на одной карте нечем.

Проверено:
 * на одном драйвере оба варианта дают одно и то же — Cd 2.39486, energy_end
   5.74813e-05; раздельный стоит 5.7% пропускной способности;
 * на сетке 2048x2048, где раздельные привязки и нужны, нативный прогон против
   контейнерного: energy_end расходится на 2.4e-07, enstrophy_end на 1.3e-07;
 * preflight в контейнере: 115 сценариев из 115, ни одного отказа (было 14);
 * 29 собственных тестов решателя зелёные.

Образ опубликован как notbigghost/kbc2d:1.2.0 (он же latest).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-31 19:08:12 +03:00
co-authored by Claude Opus 5
parent 6344c7d205
commit d5e37fcb8d
5 changed files with 311 additions and 107 deletions
+28 -29
View File
@@ -6,7 +6,7 @@
## Быстрый старт на сервере
Образ опубликован, собирать ничего не нужно: **`notbigghost/kbc2d:1.1.0`**. Исходники на
Образ опубликован, собирать ничего не нужно: **`notbigghost/kbc2d:1.2.0`**. Исходники на
сервере тоже не нужны — переносится один файл `docker-compose.server.yml`.
```sh
@@ -83,44 +83,39 @@ dzn сообщает о себе `conformanceVersion = 0.0.0.0`: набор те
Точность трансляция не портит. Но это замер на Intel; на своей карте прогоните сами — две
минуты.
### Чего этот путь НЕ может: предел 128 МиБ на привязку
### Предел 128 МиБ на привязку и как он снят
Главное ограничение dzn, и оно жёсткое. Драйвер объявляет
`max_storage_buffer_binding_size = 128 МиБ` (2²⁷ байт), тогда как нативные драйверы дают
гигабайты. Сам буфер держать можно — `max_buffer_size` у dzn 2047 МиБ, — но показать
шейдеру одной привязкой больше 128 МиБ нельзя.
Массив функций распределения занимает `nx · ny · 9 · 4` байт, значит потолок —
**3.73 млн узлов** (примерно 1920×1920). Всё, что крупнее, падает на создании bind group:
dzn объявляет `max_storage_buffer_binding_size = 128 МиБ` (2²⁷ байт), тогда как нативные
драйверы дают гигабайты. Массив популяций занимает `nx · ny · 9 · 4` байта, так что при
одной общей привязке потолок выходил **3.73 млн узлов** (примерно 1920×1920), и 14
прогонов кампании из 115 падали на создании bind group:
```
Buffer binding 0 range 150994944 exceeds `max_*_buffer_binding_size` limit 134217728
```
В кампании таких прогонов **14 из 115**, и это не мелочь: на них приходится **70.7%
стоимости**, потому что дорогие прогоны — как раз крупносеточные.
На эти 14 приходилось 70.7% стоимости кампании — дорогие прогоны как раз крупносеточные.
| прогон | сетка | буфер |
|---|---|---|
| B10_cyl_d128 | 3.93 млн узлов | 135 МБ |
| A10, A11 (турбулентность N=2048) | 4.19 млн | 144 МБ |
| D14_naca_chord192 | 4.72 млн | 162 МБ |
| I01–I09 (сверхмелкие сетки) | 8.39 млн | 288 МБ |
| A12 (турбулентность N=4096) | 16.8 млн | 576 МБ |
Существенно, что ограничена только **привязка**: `max_buffer_size` у dzn 2047 МиБ, то есть
буфер держать разрешено, нельзя лишь показать шейдеру его целиком. Поэтому решатель теперь
умеет показывать тот же буфер **девятью привязками**, по одному направлению в каждой.
Потолок поднимается в девять раз — до 33.5 млн узлов, чего хватает всей кампании с запасом
(самая крупная сетка в ней 4096×4096 — 16.8 млн узлов).
`preflight` показывает их все с причиной — запускать его до кампании обязательно.
Вариант выбирается сам, по `max_storage_buffer_binding_size` адаптера: где предела нет,
собирается прежний общий вариант без `switch` в аксессорах. Проверить оба на одной карте
можно переменной `KBC2D_SPLIT_POPULATIONS=1` — она включает раздельные привязки
принудительно.
Что с этим делать — три возможности, и ни одна не бесплатна:
Замерено на Intel Iris Xe, один драйвер, два варианта привязки:
1. **Разделить кампанию.** 101 прогон в контейнере, 14 крупных — нативной сборкой (в
Windows или на настоящем Linux-сервере). Работает сегодня, кода не трогает, но это
два разных запуска.
2. **Перестроить буферы решателя** под привязку на направление: девять привязок по
`nx · ny · 4` байта вместо одной на `nx · ny · 9 · 4`. Тогда потолок поднимается до
33.5 млн узлов и в контейнер влезает вся кампания. Цена — переделка раскладки в
ядрах WGSL (24 места обращения) и повторная валидация; на нативных драйверах смысла
в этом нет, так что понадобятся два варианта шейдера.
3. **Считать всю кампанию нативно.** Ни предела, ни платы за трансляцию — но и контейнера.
| | Cd | energy_end | MLUPS (цилиндр) |
|---|---|---|---|
| общая привязка | 2.39486 | 5.74813e-05 | 169.7 |
| девять привязок | 2.39486 | 5.74813e-05 | 160.0 |
Числа совпадают полностью; раздельный вариант стоит **5.7%** пропускной способности, и
включается только там, где без него счёт вообще невозможен.
### Чего трансляция стоит по скорости
@@ -132,6 +127,10 @@ Buffer binding 0 range 150994944 exceeds `max_*_buffer_binding_size` limit 13421
| 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×** |
| 2048×2048 | 4.19 млн | 111.8 MLUPS | 67.4 MLUPS | **1.66×** |
Последняя строка стоит особняком: там уже раздельные привязки (см. ниже), и в плату
входит их `switch` в аксессорах. Ожидаемый разброс по кампании — от 1.3× до 1.7×.
Каждый шаг решателя — несколько отправок в очередь; на мелкой сетке трансляция вызова
стоит дороже самого счёта, на крупной размазывается. Для кампании это решающее