Батчинг 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:
@@ -197,11 +197,13 @@ struct Cli {
|
||||
/// Оператор столкновения
|
||||
#[arg(long, default_value = "kbc", value_parser = ["kbc", "bgk"], help_heading = "Схема")]
|
||||
collision: String,
|
||||
/// Модель стенки на теле: bouzidi — интерполированный отскок по линкам; grad — условие
|
||||
/// Града (Dorschner и др., JFM 801 (2016)), где задаются не популяции, а целевые моменты
|
||||
/// ρ, u и тензор давлений. Авторы метода предпочитают grad: интерполяционные схемы, по их
|
||||
/// словам, «ограничены низкими Re, поскольку на границе возникают паразитные скачки».
|
||||
#[arg(long, default_value = "bouzidi", value_parser = math::WallModel::ALL,
|
||||
/// Модель стенки на теле. hrr (умолчание) — восстановление недостающих популяций по
|
||||
/// целевым моментам с рекурсивной регуляризацией до 3-го порядка Эрмита; grad — то же,
|
||||
/// но с обрывом на тензоре давлений; bouzidi — интерполированный отскок по каждому линку
|
||||
/// со своей долей пересечения q; staircase — простой отскок, q игнорируется (не для
|
||||
/// счёта, а как база сравнения). Замерено: по разрешению геометрии bouzidi точнее
|
||||
/// моментных схем, зато у тех естественная форма для подвижных стенок — см. README.
|
||||
#[arg(long, default_value = "hrr", value_parser = math::WallModel::ALL,
|
||||
help_heading = "Схема")]
|
||||
wall: String,
|
||||
/// Состав сдвиговой части KBC (табл. I 2D-статьи): n1 — только девиатор {N, Π_xy}
|
||||
@@ -451,11 +453,23 @@ enum Backend {
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
fn step(&mut self) -> StepRec {
|
||||
/// Посчитать шаг. Готовые записи ДОПИСЫВАЮТСЯ в `out` — их может быть ноль (GPU копит
|
||||
/// итоги в истории и читает пачкой) или сразу много (когда история заполнилась).
|
||||
fn advance(&mut self, out: &mut Vec<StepRec>) {
|
||||
match self {
|
||||
Backend::Cpu(s) => s.step(),
|
||||
Backend::Cpu(s) => out.push(s.step()),
|
||||
#[cfg(feature = "gpu")]
|
||||
Backend::Gpu(s) => s.step(),
|
||||
Backend::Gpu(s) => s.advance(out),
|
||||
}
|
||||
}
|
||||
/// Дочитать всё посчитанное. Обязательно перед чтением полей: иначе записи отстанут
|
||||
/// от состояния, которое покажет кадр.
|
||||
fn flush(&mut self, out: &mut Vec<StepRec>) {
|
||||
let _ = &out; // процессорный бэкенд отдаёт записи сразу, копить нечего
|
||||
match self {
|
||||
Backend::Cpu(_) => {}
|
||||
#[cfg(feature = "gpu")]
|
||||
Backend::Gpu(s) => s.flush(out),
|
||||
}
|
||||
}
|
||||
fn sample_field(&mut self, k: FieldKind) -> (Vec<R>, Vec<bool>) {
|
||||
@@ -598,11 +612,18 @@ fn run(cli: Cli) -> Result<(), String> {
|
||||
.unwrap_or(0)) as f64;
|
||||
|
||||
for t in 0..=spec.steps {
|
||||
let rec = back.step();
|
||||
recs.push(rec);
|
||||
back.advance(&mut recs);
|
||||
|
||||
if let Some(w) = writer.as_mut() {
|
||||
if t % plan.stride == 0 {
|
||||
let need_frame = writer.is_some() && t % plan.stride == 0;
|
||||
let need_report = cli.report_every > 0 && !quiet && t % cli.report_every == 0;
|
||||
// и кадр, и живая строка требуют актуальных данных — синхронизируемся только здесь
|
||||
if need_frame || need_report {
|
||||
back.flush(&mut recs);
|
||||
}
|
||||
let rec = *recs.last().unwrap_or(&StepRec::default());
|
||||
|
||||
if need_frame {
|
||||
if let Some(w) = writer.as_mut() {
|
||||
let (field, solid) = back.sample_field(field_kind);
|
||||
let (cd, _, _) =
|
||||
math::coefficients(rec.fx, rec.fy, rec.tz, spec.units.u_lat, d_ref);
|
||||
@@ -610,18 +631,25 @@ fn run(cli: Cli) -> Result<(), String> {
|
||||
.map_err(|e| format!("запись кадра: {e}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
if cli.report_every > 0 && !quiet && t % cli.report_every == 0 {
|
||||
if need_report {
|
||||
live_line(&spec, &rec, d_ref, t_start.elapsed().as_secs_f64(), nodes_per_step, full);
|
||||
}
|
||||
|
||||
// развал счёта: проверяем редко, проверка стоит полного прохода по полю
|
||||
if t % 500 == 0 && !back.is_finite() {
|
||||
// Развал счёта ловится по УЖЕ ПОСЧИТАННЫМ величинам, а не полным проходом по полю:
|
||||
// на сетке 4096×2048 такой проход тянет с устройства сотни мегабайт, и делать его
|
||||
// регулярно нельзя. Полная проверка — один раз, для подтверждения.
|
||||
if !rec.max_u.is_finite() || !rec.rho_mean.is_finite() {
|
||||
back.flush(&mut recs);
|
||||
eprintln!("\nСЧЁТ РАЗВАЛИЛСЯ на шаге {t}: в поле появились NaN/inf.");
|
||||
blew_up = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
back.flush(&mut recs);
|
||||
if !blew_up && !back.is_finite() {
|
||||
eprintln!("\nСЧЁТ РАЗВАЛИЛСЯ: итоговое поле содержит NaN/inf.");
|
||||
blew_up = true;
|
||||
}
|
||||
let wall = t_start.elapsed().as_secs_f64();
|
||||
|
||||
if let Some(w) = writer {
|
||||
@@ -729,6 +757,7 @@ fn print_header(spec: &Spec, cli: &Cli, plan: &gif::GifPlan, field: FieldKind) {
|
||||
match spec.wall {
|
||||
math::WallModel::Bouzidi => "Bouzidi",
|
||||
math::WallModel::Grad => "Град (моменты)",
|
||||
math::WallModel::Hrr => "HRR (моменты, 3-й пор.)",
|
||||
math::WallModel::Staircase => "простой отскок",
|
||||
});
|
||||
println!(" выход по u_y {:>12} губка {} столбцов ×{:.0}",
|
||||
|
||||
Reference in New Issue
Block a user