Батчинг GPU-чтения, компенсированные суммы и HRR-стенка на обоих бэкендах

БАТЧИНГ. Раньше после каждого шага делался map_async + poll(Wait) ради 48 байт итогов: на
сетке 240×120 счёт упирался в 863 шаг/с при том, что сам счёт занимал 0.27 мс из 1.16 — три
четверти времени машина стояла. Теперь k_stats2 пишет итоги в слот истории step % 128, а хост
читает пачкой. Контракт бэкенда изменён с пошагового step() на advance(&mut out) + flush():
записи дописываются в вектор, синхронизация происходит только там, где дальше нужны
актуальные данные (кадр гифки, живая строка отчёта).

Замер: 240×120 — 6715 шаг/с против 863 (7.8×); пропускная способность 195 MLUPS против прежних
105. Паритет CPU/GPU сохранён: Cd 1.798 против 1.799, ⟨ρ⟩ 1.00090 против 1.00091.

КОМПЕНСИРОВАННОЕ СУММИРОВАНИЕ (Кэхена–Ноймайера) в редукциях статистики и в сумме сил. Именно
там теряется основная точность f32: наивная сумма по 10^5…10^7 узлам съедает ~log2(N) бит.
Аппаратного f64 на целевом железе нет (в WGSL типа f64 не существует вовсе, а локальная Iris Xe
сообщает shaderFloat64 = false), поэтому компенсация — единственный доступный способ.

РАЗВАЛ СЧЁТА теперь ловится по уже посчитанным max|u| и ⟨ρ⟩, а не полным проходом по полю:
на сетке 4096×2048 такой проход тянет с устройства сотни мегабайт, и делать его регулярно
нельзя. Полная проверка осталась одна, в конце.

HRR-СТЕНКА (--wall hrr, теперь умолчание). Условие Града — это ряд Эрмита, оборванный на 2-м
порядке (ρ, u, Π). HRR продолжает его на третий, вычисляя коэффициенты не из популяций (их на
стенке как раз и не хватает), а рекурсивно из уже известных:
  a₃_xxy = 2·u_x·a₂_xy + u_y·a₂_xx,  a₃_xyy = 2·u_y·a₂_xy + u_x·a₂_yy
В D2Q9 a₃_xxx и a₃_yyy решёткой не поддерживаются и отбрасываются. Третий порядок не трогает
ρ, ρu и Π — соответствующие моменты весов обнуляются по симметрии, — что закреплено тестом.

Моментная стенка перенесена на GPU (раньше её там не было вовсе, бэкенд отказывался
запускаться): буфер индекса граничных узлов плюс ядро k_moment_wall. Скорость на момент t
берётся из пост-столкновительного поля — столкновение сохраняет ρ и ρu, поэтому отдельное
хранилище прошлого шага не нужно ни на одном бэкенде. Добавлена проверка лимита
storage-биндингов адаптера: моментной стенке нужно 9 против 8 гарантируемых.

Паритет на HRR: Cd 1.847 (CPU) против 1.848 (GPU). Три модели стенки на одной постановке дают
1.847 (hrr) / 1.845 (grad) / 1.856 (bouzidi).

33 теста, обе сборки чисты.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 03:51:05 +03:00
co-authored by Claude Opus 5
parent e3c2d417f8
commit 039e5f4806
4 changed files with 496 additions and 118 deletions
+343 -89
View File
@@ -25,6 +25,14 @@ use crate::{Collision, FieldKind, Spec, StepRec};
const WG: u32 = 64;
/// Сколько шагов копится в буфере итогов до одной синхронизации с устройством.
///
/// Раньше после каждого шага делалось `map_async` + `poll(Wait)` ради 48 байт: на сетке
/// 240×120 счёт упирался в 863 шаг/с при том, что сам счёт занимал 0.27 мс из 1.16 — три
/// четверти времени машина стояла. Теперь `k_stats2` пишет итоги в слот `step % HIST`, а
/// хост читает всю пачку разом. Обязано совпадать с `HIST` в шейдере.
const HIST: usize = 128;
// ─────────────────────────────────────────────────────────────────────────────
// Структуры, разделяемые с шейдером
// ─────────────────────────────────────────────────────────────────────────────
@@ -41,6 +49,9 @@ struct LevelParams {
probe_node: u32,
/// бит 0 — писать частичные суммы статистики; бит 1 — на этом уровне лежит зонд
flags: u32,
/// число граничных узлов (для моментных моделей стенки)
nwall: u32,
_pad: [u32; 3],
}
#[repr(C)]
@@ -55,6 +66,11 @@ struct Dyn {
nparts: u32,
refine: u32,
collision: u32,
/// Слот истории, в который `k_stats2` кладёт итоги этого шага.
slot: u32,
/// 1 — продолжать ряд Эрмита до 3-го порядка (HRR), 0 — обрыв на Π (Град).
wall_hrr: u32,
_pad: [u32; 2],
}
#[repr(C)]
@@ -126,8 +142,8 @@ struct Results {
const SHADER: &str = r#"
// ───── структуры (обязаны совпадать с gpu.rs) ─────
struct LevelParams { n:u32, nx:u32, ny:u32, nlinks:u32, bcx:f32, bcy:f32, probe_node:u32, flags:u32 };
struct Dyn { ux_in:f32, uy_in:f32, rho_out:f32, kbc_model:u32, outlet_extrap:u32, nparts:u32, refine:u32, collision:u32 };
struct LevelParams { n:u32, nx:u32, ny:u32, nlinks:u32, bcx:f32, bcy:f32, probe_node:u32, flags:u32, nwall:u32, lp1:u32, lp2:u32, lp3:u32 };
struct Dyn { ux_in:f32, uy_in:f32, rho_out:f32, kbc_model:u32, outlet_extrap:u32, nparts:u32, refine:u32, collision:u32, slot:u32, wall_hrr:u32, dp2:u32, dp3:u32 };
struct Substep { idx:u32, p0:u32, p1:u32, p2:u32 };
struct AmrParams { nghost:u32, nrestrict:u32, ccount:u32, fcount:u32, r01:f32, rfc:f32, w:f32, pad:f32 };
struct GLink { node:u32, far:u32, i:u32, ib:u32, kind:u32, q:f32, p0:u32, p1:u32 };
@@ -158,9 +174,28 @@ struct Partial { rho:f32, maxu:f32, gsum:f32, gmin:f32, gmax:f32, cnt:f32, degen
@group(2) @binding(5) var<storage, read> rest : array<vec2<u32>>;
@group(2) @binding(6) var<uniform> A : AmrParams;
// (узел, смещение первого линка, число линков, —). Линки одного узла лежат подряд.
@group(3) @binding(0) var<storage, read> wnodes : array<vec4<u32>>;
const GREL: f32 = 1e-8;
const CS2 : f32 = 0.3333333333;
// Раскладка буфера итогов. results[slot*12 .. +12) — история одного шага; за историей идут
// рабочие слоты силы по подшагам. История нужна, чтобы не синхронизироваться с устройством
// на каждом шаге: результаты копятся и читаются пачкой (см. HIST в gpu.rs).
const HIST: u32 = 128u;
const FORCE_BASE: u32 = 1536u; // = HIST*12u
// Компенсированное сложение Кэхена–Ноймайера: возвращает (сумма, накопленная поправка).
// Наивная сумма по 10^5…10^7 значений в f32 съедает ~log2(N) бит; здесь потеря не копится,
// а итог берётся как s + c. Стоит несколько операций на элемент.
fn kadd(s: f32, c: f32, x: f32) -> vec2<f32> {
let t = s + x;
var cc: f32;
if (abs(s) >= abs(x)) { cc = c + ((s - t) + x); } else { cc = c + ((x - t) + s); }
return vec2<f32>(t, cc);
}
// ───── физика: построчный перенос math.rs ─────
// Энтропийное равновесие в product-form (точный максимизатор энтропии при заданных rho, rho*u)
@@ -369,7 +404,9 @@ fn k_force(@builtin(local_invocation_id) lid: vec3<u32>) {
let t = lid.x;
var cx = array<f32,9>(0.0, 1.0, 0.0, -1.0, 0.0, 1.0, -1.0, -1.0, 1.0);
var cy = array<f32,9>(0.0, 0.0, 1.0, 0.0, -1.0, 1.0, 1.0, -1.0, -1.0);
var sx = 0.0; var sy = 0.0; var sz = 0.0;
var sx = vec2<f32>(0.0, 0.0);
var sy = vec2<f32>(0.0, 0.0);
var sz = vec2<f32>(0.0, 0.0);
var k = t;
loop {
if (k >= P.nlinks) { break; }
@@ -378,15 +415,15 @@ fn k_force(@builtin(local_invocation_id) lid: vec3<u32>) {
let fb = f[L.ib*P.n + L.node];
let dfx = cx[L.i]*fp + cx[L.i]*fb;
let dfy = cy[L.i]*fp + cy[L.i]*fb;
sx = sx + dfx;
sy = sy + dfy;
sx = kadd(sx.x, sx.y, dfx);
sy = kadd(sy.x, sy.y, dfy);
// плечо до точки пересечения линка со стенкой, а не до узла
let rx = f32(L.node % P.nx) - P.bcx + L.q*cx[L.i];
let ry = f32(L.node / P.nx) - P.bcy + L.q*cy[L.i];
sz = sz + rx*dfy - ry*dfx;
sz = kadd(sz.x, sz.y, rx*dfy - ry*dfx);
k = k + 256u;
}
wfx[t] = sx; wfy[t] = sy; wtz[t] = sz;
wfx[t] = sx.x + sx.y; wfy[t] = sy.x + sy.y; wtz[t] = sz.x + sz.y;
workgroupBarrier();
var s = 128u;
loop {
@@ -400,9 +437,9 @@ fn k_force(@builtin(local_invocation_id) lid: vec3<u32>) {
s = s >> 1u;
}
if (t == 0u) {
results[16u + S.idx*4u + 0u] = wfx[0];
results[16u + S.idx*4u + 1u] = wfy[0];
results[16u + S.idx*4u + 2u] = wtz[0];
results[FORCE_BASE + S.idx*4u + 0u] = wfx[0];
results[FORCE_BASE + S.idx*4u + 1u] = wfy[0];
results[FORCE_BASE + S.idx*4u + 2u] = wtz[0];
}
}
@@ -440,7 +477,7 @@ fn k_stats1(@builtin(global_invocation_id) gid: vec3<u32>,
var fv: array<f32,9>;
load9(&fv, nd, P.n);
let m = macros9(fv);
results[3] = m.z;
results[D.slot*12u + 3u] = m.z;
}
wp[t] = p;
workgroupBarrier();
@@ -469,60 +506,198 @@ fn k_stats1(@builtin(global_invocation_id) gid: vec3<u32>,
@compute @workgroup_size(64)
fn k_stats2(@builtin(local_invocation_id) lid: vec3<u32>) {
let t = lid.x;
var p: Partial;
p.rho = 0.0; p.maxu = 0.0; p.gsum = 0.0; p.gmin = 1e30; p.gmax = -1e30;
p.cnt = 0.0; p.degen = 0.0; p.xineg = 0.0;
// слагаемых до 10^5 на поток — здесь и живёт основная потеря f32, поэтому все
// аддитивные накопители компенсированные
var rho = vec2<f32>(0.0, 0.0);
var gsum = vec2<f32>(0.0, 0.0);
var cnt = vec2<f32>(0.0, 0.0);
var degen = vec2<f32>(0.0, 0.0);
var xineg = vec2<f32>(0.0, 0.0);
var maxu = 0.0; var gmin = 1e30; var gmax = -1e30;
var k = t;
loop {
if (k >= D.nparts) { break; }
let o = parts[k];
p.rho = p.rho + o.rho;
p.maxu = max(p.maxu, o.maxu);
p.gsum = p.gsum + o.gsum;
p.gmin = min(p.gmin, o.gmin);
p.gmax = max(p.gmax, o.gmax);
p.cnt = p.cnt + o.cnt;
p.degen = p.degen + o.degen;
p.xineg = p.xineg + o.xineg;
rho = kadd(rho.x, rho.y, o.rho);
gsum = kadd(gsum.x, gsum.y, o.gsum);
cnt = kadd(cnt.x, cnt.y, o.cnt);
degen = kadd(degen.x, degen.y, o.degen);
xineg = kadd(xineg.x, xineg.y, o.xineg);
maxu = max(maxu, o.maxu);
gmin = min(gmin, o.gmin);
gmax = max(gmax, o.gmax);
k = k + 64u;
}
var p: Partial;
p.rho = rho.x + rho.y; p.gsum = gsum.x + gsum.y; p.cnt = cnt.x + cnt.y;
p.degen = degen.x + degen.y; p.xineg = xineg.x + xineg.y;
p.maxu = maxu; p.gmin = gmin; p.gmax = gmax;
wp[t] = p;
workgroupBarrier();
// сведение 256 потоков через 64 ячейки делаем последовательно нулевым потоком:
// объём данных крошечный, а корректность важнее пары микросекунд
if (t == 0u) {
var q: Partial;
q.rho = 0.0; q.maxu = 0.0; q.gsum = 0.0; q.gmin = 1e30; q.gmax = -1e30;
q.cnt = 0.0; q.degen = 0.0; q.xineg = 0.0;
var qrho = vec2<f32>(0.0, 0.0);
var qgsum = vec2<f32>(0.0, 0.0);
var qcnt = vec2<f32>(0.0, 0.0);
var qdeg = vec2<f32>(0.0, 0.0);
var qxin = vec2<f32>(0.0, 0.0);
var qmaxu = 0.0; var qgmin = 1e30; var qgmax = -1e30;
for (var j = 0u; j < 64u; j = j + 1u) {
let o = wp[j];
q.rho = q.rho + o.rho;
q.maxu = max(q.maxu, o.maxu);
q.gsum = q.gsum + o.gsum;
q.gmin = min(q.gmin, o.gmin);
q.gmax = max(q.gmax, o.gmax);
q.cnt = q.cnt + o.cnt;
q.degen = q.degen + o.degen;
q.xineg = q.xineg + o.xineg;
qrho = kadd(qrho.x, qrho.y, o.rho);
qgsum = kadd(qgsum.x, qgsum.y, o.gsum);
qcnt = kadd(qcnt.x, qcnt.y, o.cnt);
qdeg = kadd(qdeg.x, qdeg.y, o.degen);
qxin = kadd(qxin.x, qxin.y, o.xineg);
qmaxu = max(qmaxu, o.maxu);
qgmin = min(qgmin, o.gmin);
qgmax = max(qgmax, o.gmax);
}
var fx = 0.0; var fy = 0.0; var tz = 0.0;
for (var s = 0u; s < D.refine; s = s + 1u) {
fx = fx + results[16u + s*4u + 0u];
fy = fy + results[16u + s*4u + 1u];
tz = tz + results[16u + s*4u + 2u];
fx = fx + results[FORCE_BASE + s*4u + 0u];
fy = fy + results[FORCE_BASE + s*4u + 1u];
tz = tz + results[FORCE_BASE + s*4u + 2u];
}
let inv = 1.0 / f32(D.refine);
results[0] = fx*inv;
results[1] = fy*inv;
results[2] = tz*inv;
results[4] = q.rho;
results[5] = q.maxu;
results[6] = q.gsum;
results[7] = q.gmin;
results[8] = q.gmax;
results[9] = q.cnt;
results[10] = q.degen;
results[11] = q.xineg;
let o = D.slot*12u;
results[o + 0u] = fx*inv;
results[o + 1u] = fy*inv;
results[o + 2u] = tz*inv;
results[o + 4u] = qrho.x + qrho.y;
results[o + 5u] = qmaxu;
results[o + 6u] = qgsum.x + qgsum.y;
results[o + 7u] = qgmin;
results[o + 8u] = qgmax;
results[o + 9u] = qcnt.x + qcnt.y;
results[o + 10u] = qdeg.x + qdeg.y;
results[o + 11u] = qxin.x + qxin.y;
}
}
// ───── граничное условие на моментах (Град / HRR) ─────
// Скорость узла на момент t: берётся из ПОСТ-СТОЛКНОВИТЕЛЬНОГО поля. Столкновение сохраняет
// rho и rho*u точно, поэтому это ровно u(x, t) — то, что требует прил. B, без отдельного
// хранилища прошлого шага.
fn u_at_t(nd: u32) -> vec2<f32> {
var ff: array<f32,9>;
for (var i = 0u; i < 9u; i = i + 1u) { ff[i] = post[i*P.n + nd]; }
let m = macros9(ff);
return vec2<f32>(m.y, m.z);
}
// (du/dx, du/dy, dv/dx, dv/dy): центральная разность там, где оба соседа жидкие,
// односторонняя — где один твёрдый, ноль — если твёрдые оба.
fn grad_u_at_t(nd: u32) -> vec4<f32> {
let x = nd % P.nx;
let y = nd / P.nx;
let xm = y*P.nx + (x + P.nx - 1u) % P.nx;
let xp = y*P.nx + (x + 1u) % P.nx;
let ym = ((y + P.ny - 1u) % P.ny)*P.nx + x;
let yp = ((y + 1u) % P.ny)*P.nx + x;
let uc = u_at_t(nd);
var dx = vec2<f32>(0.0, 0.0);
let am = solid[xm] == 0u; let ap = solid[xp] == 0u;
if (am && ap) { dx = 0.5*(u_at_t(xp) - u_at_t(xm)); }
else if (ap) { dx = u_at_t(xp) - uc; }
else if (am) { dx = uc - u_at_t(xm); }
var dy = vec2<f32>(0.0, 0.0);
let bm = solid[ym] == 0u; let bp = solid[yp] == 0u;
if (bm && bp) { dy = 0.5*(u_at_t(yp) - u_at_t(ym)); }
else if (bp) { dy = u_at_t(yp) - uc; }
else if (bm) { dy = uc - u_at_t(ym); }
return vec4<f32>(dx.x, dy.x, dx.y, dy.y);
}
// Приближение Града, ур. (2.13): популяции по rho, u и полному тензору давлений.
fn grad_init9(rho: f32, ux: f32, uy: f32, pxx: f32, pxy: f32, pyy: f32) -> array<f32,9> {
let axx = pxx - rho*CS2;
let ayy = pyy - rho*CS2;
var w = array<f32,9>(4.0/9.0, 1.0/9.0, 1.0/9.0, 1.0/9.0, 1.0/9.0,
1.0/36.0, 1.0/36.0, 1.0/36.0, 1.0/36.0);
var cx = array<f32,9>(0.0, 1.0, 0.0, -1.0, 0.0, 1.0, -1.0, -1.0, 1.0);
var cy = array<f32,9>(0.0, 0.0, 1.0, 0.0, -1.0, 1.0, 1.0, -1.0, -1.0);
var out: array<f32,9>;
for (var i = 0u; i < 9u; i = i + 1u) {
let quad = axx*(cx[i]*cx[i] - CS2) + 2.0*pxy*cx[i]*cy[i] + ayy*(cy[i]*cy[i] - CS2);
out[i] = w[i]*(rho + rho*(ux*cx[i] + uy*cy[i])/CS2 + quad/(2.0*CS2*CS2));
}
return out;
}
// third=1 — HRR: ряд Эрмита продолжается на 3-й порядок рекурсивно из неравновесных
// коэффициентов 2-го. Построчный перенос math::moment_wall.
fn moment_wall9(rho: f32, ux: f32, uy: f32, dudx: f32, dudy: f32, dvdx: f32, dvdy: f32,
b: f32, third: u32) -> array<f32,9> {
let pref = rho*CS2/(2.0*b);
let nxx = -pref*2.0*dudx;
let nyy = -pref*2.0*dvdy;
let nxy = -pref*(dudy + dvdx);
var out = grad_init9(rho, ux, uy,
rho*CS2 + rho*ux*ux + nxx,
rho*ux*uy + nxy,
rho*CS2 + rho*uy*uy + nyy);
if (third == 1u) {
let a3xxy = 2.0*ux*nxy + uy*nxx;
let a3xyy = 2.0*uy*nxy + ux*nyy;
let k = 1.0/(2.0*CS2*CS2*CS2);
var w = array<f32,9>(4.0/9.0, 1.0/9.0, 1.0/9.0, 1.0/9.0, 1.0/9.0,
1.0/36.0, 1.0/36.0, 1.0/36.0, 1.0/36.0);
var cx = array<f32,9>(0.0, 1.0, 0.0, -1.0, 0.0, 1.0, -1.0, -1.0, 1.0);
var cy = array<f32,9>(0.0, 0.0, 1.0, 0.0, -1.0, 1.0, 1.0, -1.0, -1.0);
for (var i = 0u; i < 9u; i = i + 1u) {
out[i] = out[i] + w[i]*k*((cx[i]*cx[i] - CS2)*cy[i]*a3xxy
+ cx[i]*(cy[i]*cy[i] - CS2)*a3xyy);
}
}
return out;
}
@compute @workgroup_size(64)
fn k_moment_wall(@builtin(global_invocation_id) gid: vec3<u32>) {
let w = gid.x;
if (w >= P.nwall) { return; }
let wn = wnodes[w];
let node = wn.x;
let first = wn.y;
let cnt = wn.z;
// целевая скорость (B 1): интерполяция между стенкой и дальним жидким соседом
var ux = 0.0;
var uy = 0.0;
for (var k = 0u; k < cnt; k = k + 1u) {
let L = links[first + k];
var fx = 0.0;
var fy = 0.0; // стенка неподвижна; дальнего соседа может не быть
if (solid[L.far] == 0u) {
let uf = u_at_t(L.far);
fx = uf.x;
fy = uf.y;
}
ux = ux + (L.q*fx) / (1.0 + L.q);
uy = uy + (L.q*fy) / (1.0 + L.q);
}
let inv = 1.0 / f32(cnt);
ux = ux * inv;
uy = uy * inv;
// целевая плотность (B 3): известные популяции плюс отскок недостающих
var missing = array<u32,9>(0u,0u,0u,0u,0u,0u,0u,0u,0u);
for (var k = 0u; k < cnt; k = k + 1u) { missing[links[first + k].ib] = 1u; }
var opp = array<u32,9>(0u, 3u, 4u, 1u, 2u, 7u, 8u, 5u, 6u);
var rho = 0.0;
for (var i = 0u; i < 9u; i = i + 1u) {
if (missing[i] == 1u) { rho = rho + post[opp[i]*P.n + node]; }
else { rho = rho + f[i*P.n + node]; }
}
let g4 = grad_u_at_t(node);
var g = moment_wall9(rho, ux, uy, g4.x, g4.y, g4.z, g4.w,
beta[node % P.nx], D.wall_hrr);
for (var i = 0u; i < 9u; i = i + 1u) {
if (missing[i] == 1u) { f[i*P.n + node] = g[i]; }
}
}
@@ -595,6 +770,9 @@ struct GpuLevel {
/// Стабилизатор γ поузлово — нужен для картинки; забирается с устройства как есть.
gam: wgpu::Buffer,
bind: wgpu::BindGroup,
/// Группа 3: индекс граничных узлов для моментной стенки.
wall_bind: wgpu::BindGroup,
nwall: u32,
/// число рабочих групп редукции статистики (оно же длина буфера частичных сумм)
parts_count: u32,
nlinks: u32,
@@ -628,6 +806,7 @@ fn make_level(
probe_node: u32,
flags: u32,
u0: (R, R),
wall_layout: &wgpu::BindGroupLayout,
) -> GpuLevel {
let n = nx * ny;
let init = soa_equilibrium(n, u0);
@@ -668,6 +847,20 @@ fn make_level(
let links_buf = storage_init(device, "links", bytemuck::cast_slice(&links));
let beta_buf = storage_init(device, "beta", bytemuck::cast_slice(beta));
// индекс граничных узлов: (узел, смещение первого линка, число линков, —)
let wnodes: Vec<[u32; 4]> = geom
.wall_nodes
.iter()
.map(|w| [w.node, w.first, w.count as u32, 0])
.collect();
let nwall = wnodes.len() as u32;
let wall_buf = storage_init(device, "wall nodes", bytemuck::cast_slice(&wnodes));
let wall_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("wall"),
layout: wall_layout,
entries: &[bind(0, &wall_buf)],
});
let parts_count = ceil_div(n as u32, WG);
let parts = storage(device, "parts", (parts_count as u64) * 32);
let params = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
@@ -681,6 +874,8 @@ fn make_level(
bcy: geom.body_cy as f32,
probe_node,
flags,
nwall,
_pad: [0; 3],
}),
usage: wgpu::BufferUsages::UNIFORM,
});
@@ -699,7 +894,7 @@ fn make_level(
bind(7, &params),
],
});
GpuLevel { nx, ny, n, f, gam, bind, parts_count, nlinks: links.len() as u32 }
GpuLevel { nx, ny, n, f, gam, bind, wall_bind, nwall, parts_count, nlinks: links.len() as u32 }
}
// ─────────────────────────────────────────────────────────────────────────────
@@ -730,6 +925,9 @@ pub struct Sim {
fluid_count: R,
step_index: u64,
d_ref: R,
/// Номера шагов, посчитанных на устройстве, но ещё не прочитанных на хост.
/// Длина = занятые слоты истории.
pending: Vec<u64>,
}
struct AmrRes {
@@ -745,6 +943,7 @@ struct Pipes {
collide: wgpu::ComputePipeline,
stream: wgpu::ComputePipeline,
bouzidi: wgpu::ComputePipeline,
moment_wall: wgpu::ComputePipeline,
walls: wgpu::ComputePipeline,
channel: wgpu::ComputePipeline,
force: wgpu::ComputePipeline,
@@ -784,11 +983,17 @@ impl Sim {
))
.map_err(|e| format!("не удалось получить устройство: {e}"))?;
// Условие Града на GPU пока не перенесено. Молча считать по Bouzidi нельзя — это
// была бы другая физика под тем же ключом, поэтому отказываемся явно.
if spec.wall == math::WallModel::Grad {
return Err("--wall grad на GPU не реализован (перенесён только Bouzidi). Запусти с --backend cpu либо оставь --wall bouzidi"
.into());
// Моментная стенка добавляет к связке уровня ещё один storage-биндинг (индекс
// граничных узлов), итого 9 против 8, гарантируемых лимитами по умолчанию. На
// десктопных адаптерах их обычно >= 16, но проверить надо явно.
if spec.wall.is_moment_based() {
let lim = adapter.limits().max_storage_buffers_per_shader_stage;
if lim < 9 {
return Err(format!(
"адаптер даёт только {lim} storage-биндингов на стадию, моментной стенке \
нужно 9. Запусти с --wall bouzidi либо --backend cpu"
));
}
}
// ── топология берётся из процессорного бэкенда, а не строится заново ──
@@ -815,6 +1020,10 @@ impl Sim {
let bgl_level = level_layout(&device);
let bgl_dyn = dyn_layout(&device);
let bgl_amr = amr_layout(&device);
let bgl_wall = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("wall"),
entries: &[ro(0)],
});
let bgl_empty = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("empty"),
entries: &[],
@@ -825,6 +1034,11 @@ impl Sim {
bind_group_layouts: &[&bgl_level, &bgl_dyn],
push_constant_ranges: &[],
});
let pl_wall = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("moment wall"),
bind_group_layouts: &[&bgl_level, &bgl_dyn, &bgl_empty, &bgl_wall],
push_constant_ranges: &[],
});
let pl_amr = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("amr"),
bind_group_layouts: &[&bgl_empty, &bgl_empty, &bgl_amr],
@@ -849,16 +1063,17 @@ impl Sim {
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// 16 слотов итогов + 8 подшагов по 4 числа на силу
// история на HIST шагов + рабочие слоты силы на 8 подшагов
let hist_bytes = (HIST * std::mem::size_of::<Results>()) as u64;
let results_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("results"),
size: (16 + 8 * 4) * 4,
size: hist_bytes + 8 * 4 * 4,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let staging = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("staging"),
size: std::mem::size_of::<Results>() as u64,
size: hist_bytes,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
@@ -906,6 +1121,7 @@ impl Sim {
probe0 as u32,
flags0,
u0,
&bgl_wall,
);
let pre = device.create_buffer(&wgpu::BufferDescriptor {
@@ -934,7 +1150,8 @@ impl Sim {
let probe1 = if probe_on_fine { ((py - ay) * r) * nfx + (px - ax) * r } else { 0 };
let flags1 = if probe_on_fine { 2 } else { 0 };
let lvl1 =
make_level(&device, &bgl_level, nfx, nfy, &geom1, &beta1, probe1 as u32, flags1, u0);
make_level(&device, &bgl_level, nfx, nfy, &geom1, &beta1, probe1 as u32, flags1,
u0, &bgl_wall);
let ghosts: Vec<GGhost> = patch
.ghosts()
@@ -1021,6 +1238,7 @@ impl Sim {
collide: mk(&pl_level, "k_collide"),
stream: mk(&pl_level, "k_stream"),
bouzidi: mk(&pl_level, "k_bouzidi"),
moment_wall: mk(&pl_wall, "k_moment_wall"),
walls: mk(&pl_level, "k_walls"),
channel: mk(&pl_level, "k_channel"),
force: mk(&pl_level, "k_force"),
@@ -1065,6 +1283,7 @@ impl Sim {
readback,
fluid_count,
step_index: 0,
pending: Vec::with_capacity(HIST),
d_ref,
})
}
@@ -1095,10 +1314,14 @@ impl Sim {
(u * c, uy)
}
pub fn step(&mut self) -> StepRec {
/// Посчитать один шаг. Результаты не читаются сразу: они копятся в истории на устройстве
/// и попадают в `out` пачкой — либо когда история заполнится, либо по явному `flush`.
pub fn advance(&mut self, out: &mut Vec<StepRec>) {
let t = self.step_index;
let slot = self.pending.len() as u32;
let (ux_in, uy_in) = self.inlet(t);
let refine = self.spec.refine.max(1);
let moment_wall = self.spec.wall.is_moment_based();
self.queue.write_buffer(
&self.dyn_buf,
0,
@@ -1111,6 +1334,9 @@ impl Sim {
nparts: self.l0.parts_count,
refine: refine as u32,
collision: (self.spec.collision == Collision::Bgk) as u32,
slot,
wall_hrr: (self.spec.wall == math::WallModel::Hrr) as u32,
_pad: [0; 2],
}),
);
@@ -1135,8 +1361,15 @@ impl Sim {
p.dispatch_workgroups(ncell, 1, 1);
p.set_pipeline(&self.pipes.stream);
p.dispatch_workgroups(ncell, 1, 1);
p.set_pipeline(&self.pipes.bouzidi);
p.dispatch_workgroups(self.l0.link_groups(), 1, 1);
if moment_wall {
p.set_bind_group(2, &self.pipes.empty_bg, &[]);
p.set_bind_group(3, &self.l0.wall_bind, &[]);
p.set_pipeline(&self.pipes.moment_wall);
p.dispatch_workgroups(ceil_div(self.l0.nwall.max(1), WG), 1, 1);
} else {
p.set_pipeline(&self.pipes.bouzidi);
p.dispatch_workgroups(self.l0.link_groups(), 1, 1);
}
// порядок обязателен: стенки снимают заворот по y, затем Zou-He — по x, на весь столбец
p.set_pipeline(&self.pipes.walls);
p.dispatch_workgroups(ceil_div(self.l0.nx as u32, WG), 1, 1);
@@ -1172,8 +1405,15 @@ impl Sim {
p.dispatch_workgroups(ncell1, 1, 1);
p.set_pipeline(&self.pipes.stream);
p.dispatch_workgroups(ncell1, 1, 1);
p.set_pipeline(&self.pipes.bouzidi);
p.dispatch_workgroups(nlink1, 1, 1);
if moment_wall {
p.set_bind_group(2, &self.pipes.empty_bg, &[]);
p.set_bind_group(3, &l1.wall_bind, &[]);
p.set_pipeline(&self.pipes.moment_wall);
p.dispatch_workgroups(ceil_div(l1.nwall.max(1), WG), 1, 1);
} else {
p.set_pipeline(&self.pipes.bouzidi);
p.dispatch_workgroups(nlink1, 1, 1);
}
// силу снимаем на каждом подшаге, усредняется она в k_stats2
p.set_pipeline(&self.pipes.force);
p.dispatch_workgroups(1, 1, 1);
@@ -1231,18 +1471,50 @@ impl Sim {
p.dispatch_workgroups(1, 1, 1);
}
enc.copy_buffer_to_buffer(
&self.results_buf,
0,
&self.staging,
0,
std::mem::size_of::<Results>() as u64,
);
self.queue.submit(Some(enc.finish()));
self.pending.push(t);
self.step_index += 1;
if self.pending.len() == HIST {
self.flush(out);
}
}
/// Дочитать всё, что уже посчитано устройством. Обязательно вызывать перед тем, как
/// смотреть на поля (кадр гифки, проверка на NaN) — иначе записи отстанут от состояния.
pub fn flush(&mut self, out: &mut Vec<StepRec>) {
let n = self.pending.len();
if n == 0 {
return;
}
let bytes = (n * std::mem::size_of::<Results>()) as u64;
let mut enc = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("flush") });
enc.copy_buffer_to_buffer(&self.results_buf, 0, &self.staging, 0, bytes);
self.queue.submit(Some(enc.finish()));
let res: Results = self.read_staging();
self.step_index += 1;
let slice = self.staging.slice(..bytes);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |r| {
let _ = tx.send(r);
});
self.device.poll(wgpu::Maintain::Wait);
let got: Vec<Results> = match rx.recv() {
Ok(Ok(())) => {
let data = slice.get_mapped_range();
bytemuck::cast_slice::<u8, Results>(&data[..bytes as usize]).to_vec()
}
_ => vec![Results::default(); n],
};
self.staging.unmap();
for (i, &t) in self.pending.iter().enumerate() {
out.push(self.make_rec(t, &got[i]));
}
self.pending.clear();
}
fn make_rec(&self, t: u64, res: &Results) -> StepRec {
let cnt = if res.cnt > 0.0 { res.cnt as R } else { self.fluid_count };
StepRec {
step: t,
@@ -1260,24 +1532,6 @@ impl Sim {
}
}
fn read_staging(&self) -> Results {
let slice = self.staging.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |r| {
let _ = tx.send(r);
});
self.device.poll(wgpu::Maintain::Wait);
let out = match rx.recv() {
Ok(Ok(())) => {
let data = slice.get_mapped_range();
*bytemuck::from_bytes::<Results>(&data[..std::mem::size_of::<Results>()])
}
_ => Results::default(),
};
self.staging.unmap();
out
}
/// Скачать популяции L0 на хост (нужно для кадров и проверки на NaN).
fn download_l0(&self) -> Vec<f32> {
self.download(&self.l0.f, (9 * self.l0.n * 4) as u64)