1913 lines
90 KiB
Python
1913 lines
90 KiB
Python
|
|
"""Reusable GPU backend for the AirfRANS RANS solver verifier.
|
||
|
|
|
||
|
|
The verifier under ``scripts/`` is an acceptance harness. This module owns the
|
||
|
|
GPU backend contract, GPU-side state transfer, CUDA solver-stage graph, and
|
||
|
|
backend diagnostics used by ``--backend gpu``.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import dataclasses
|
||
|
|
import math
|
||
|
|
import os
|
||
|
|
import subprocess
|
||
|
|
import time
|
||
|
|
from collections.abc import Iterable, Mapping
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
import numpy as np
|
||
|
|
import quadrants as qd
|
||
|
|
|
||
|
|
from .constants import (
|
||
|
|
DEFAULT_LAMINAR_NU,
|
||
|
|
DEFAULT_MOMENTUM_PBICGSTAB_ITERATIONS,
|
||
|
|
DEFAULT_MOMENTUM_PBICGSTAB_RESIDUAL_TOLERANCE_SQUARED,
|
||
|
|
DEFAULT_PRESSURE_CG_ITERATIONS,
|
||
|
|
DEFAULT_MOMENTUM_RELAXATION_ALPHA,
|
||
|
|
DEFAULT_SIMPLE_CONSISTENT_RATU_FACTOR,
|
||
|
|
DEFAULT_OMEGA_WALL_BETA1,
|
||
|
|
FULL_GPU_RANS_GUARD,
|
||
|
|
GPU_BACKEND_PROVIDER,
|
||
|
|
GPU_INPUT_SCHEMA_VERSION,
|
||
|
|
GPU_KERNELS_MISSING,
|
||
|
|
GPU_NUMERICAL_MISMATCH,
|
||
|
|
GPU_PRIMITIVE_NAME,
|
||
|
|
GPU_PRIMITIVE_PROOF,
|
||
|
|
GPU_RUNTIME_UNAVAILABLE,
|
||
|
|
GPU_STAGE_CAPABILITY_PREFIX,
|
||
|
|
GPU_STAGE_KERNEL_ENTRYPOINTS,
|
||
|
|
GPU_STAGE_UNSUPPORTED,
|
||
|
|
REQUIRED_FIELDS,
|
||
|
|
STAGE_OBSERVABILITY_GROUPS,
|
||
|
|
TURBULENCE_FIELDS,
|
||
|
|
)
|
||
|
|
from .kernels import (
|
||
|
|
gpu_rans_face_flux_copy,
|
||
|
|
gpu_rans_final_correction,
|
||
|
|
gpu_rans_momentum_assembly,
|
||
|
|
gpu_rans_momentum_diffusion_coefficients,
|
||
|
|
gpu_rans_momentum_wall_diffusion_coefficients,
|
||
|
|
gpu_rans_momentum_convection_coefficients,
|
||
|
|
gpu_rans_momentum_equation_relaxation,
|
||
|
|
gpu_rans_momentum_hbyA_face_accumulate,
|
||
|
|
gpu_rans_momentum_hbyA_finish,
|
||
|
|
gpu_rans_momentum_hbyA_fixed_value_boundary,
|
||
|
|
gpu_rans_momentum_hbyA_source,
|
||
|
|
gpu_rans_pressure_assembly,
|
||
|
|
gpu_rans_pressure_inputs,
|
||
|
|
gpu_rans_consistent_rAtU,
|
||
|
|
gpu_rans_pressure_laplacian_coefficients,
|
||
|
|
gpu_rans_pressure_flux_correction,
|
||
|
|
gpu_rans_pressure_source_from_flux,
|
||
|
|
gpu_rans_pressure_source_from_boundary_flux,
|
||
|
|
gpu_rans_pressure_mixed_boundary_laplacian,
|
||
|
|
gpu_rans_surface_flux_from_cells,
|
||
|
|
gpu_rans_turbulence_update,
|
||
|
|
gpu_rans_omega_wall_update,
|
||
|
|
gpu_rans_pressure_velocity_correction,
|
||
|
|
)
|
||
|
|
from .linear_solve import (
|
||
|
|
gpu_ldu_pbicgstab_vector_asymmetric_faces,
|
||
|
|
gpu_ldu_pcg_scalar_symmetric_faces,
|
||
|
|
)
|
||
|
|
|
||
|
|
BACKEND_CHOICES = ("auto", "cpu", "gpu")
|
||
|
|
BACKEND_FAILURE = "backend_execution_failure"
|
||
|
|
COMPARISON_FAILURE = "numerical_comparison_failure"
|
||
|
|
|
||
|
|
FIELD_COMPARISON_ATTRIBUTION = {
|
||
|
|
"U": ("final_correction", "linear_solve_results", "momentum_assembly", "turbulence_updates"),
|
||
|
|
"p": ("linear_solve_results", "pressure_assembly", "final_correction"),
|
||
|
|
"phi": ("final_correction", "linear_solve_results", "pressure_assembly"),
|
||
|
|
"nut": ("turbulence_updates",),
|
||
|
|
"k": ("turbulence_updates",),
|
||
|
|
"omega": ("turbulence_updates",),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
class BackendExecutionError(Exception):
|
||
|
|
"""Backend selection or execution failure before numerical comparison."""
|
||
|
|
|
||
|
|
def __init__(self, step: str, message: str, *, details: Mapping[str, Any] | None = None) -> None:
|
||
|
|
super().__init__(message)
|
||
|
|
self.step = step
|
||
|
|
self.details = dict(details or {})
|
||
|
|
|
||
|
|
|
||
|
|
@dataclasses.dataclass(frozen=True)
|
||
|
|
class GpuSourceLocation:
|
||
|
|
file: str
|
||
|
|
function: str
|
||
|
|
lines: tuple[int, int] | None = None
|
||
|
|
|
||
|
|
|
||
|
|
@dataclasses.dataclass(frozen=True)
|
||
|
|
class GpuTransformResult:
|
||
|
|
name: str
|
||
|
|
phase: str
|
||
|
|
source: GpuSourceLocation
|
||
|
|
inputs: Mapping[str, Any]
|
||
|
|
outputs: Mapping[str, Any]
|
||
|
|
changed_fields: list[str]
|
||
|
|
metadata: Mapping[str, Any]
|
||
|
|
|
||
|
|
|
||
|
|
@dataclasses.dataclass(frozen=True)
|
||
|
|
class GpuPatchField:
|
||
|
|
name: str
|
||
|
|
type: str
|
||
|
|
values: np.ndarray
|
||
|
|
fixes_value: bool
|
||
|
|
assignable: bool
|
||
|
|
coupled: bool
|
||
|
|
updated: bool
|
||
|
|
patch_internal: np.ndarray | None
|
||
|
|
value_internal_coeffs: np.ndarray | None
|
||
|
|
value_boundary_coeffs: np.ndarray | None
|
||
|
|
gradient_internal_coeffs: np.ndarray | None
|
||
|
|
gradient_boundary_coeffs: np.ndarray | None
|
||
|
|
|
||
|
|
|
||
|
|
@dataclasses.dataclass(frozen=True)
|
||
|
|
class GpuField:
|
||
|
|
name: str
|
||
|
|
kind: str
|
||
|
|
dimensions: str
|
||
|
|
entity_kind: str
|
||
|
|
entity_count: int
|
||
|
|
internal: np.ndarray
|
||
|
|
boundary: Mapping[str, GpuPatchField]
|
||
|
|
|
||
|
|
|
||
|
|
def json_ready(value: Any) -> Any:
|
||
|
|
"""Convert report values into strict JSON-compatible data."""
|
||
|
|
|
||
|
|
if dataclasses.is_dataclass(value) and not isinstance(value, type):
|
||
|
|
return json_ready(dataclasses.asdict(value))
|
||
|
|
if isinstance(value, Path):
|
||
|
|
return str(value)
|
||
|
|
if isinstance(value, np.ndarray):
|
||
|
|
return array_stats(value)
|
||
|
|
if isinstance(value, np.generic):
|
||
|
|
return json_ready(value.item())
|
||
|
|
if isinstance(value, float):
|
||
|
|
return value if math.isfinite(value) else None
|
||
|
|
if isinstance(value, (str, int, bool)) or value is None:
|
||
|
|
return value
|
||
|
|
if isinstance(value, Mapping):
|
||
|
|
return {str(key): json_ready(item) for key, item in value.items()}
|
||
|
|
if isinstance(value, (list, tuple, set)):
|
||
|
|
return [json_ready(item) for item in value]
|
||
|
|
return repr(value)
|
||
|
|
|
||
|
|
|
||
|
|
def array_shape(array: Any | None) -> list[int] | None:
|
||
|
|
if array is None:
|
||
|
|
return None
|
||
|
|
return [int(dim) for dim in np.asarray(array).shape]
|
||
|
|
|
||
|
|
|
||
|
|
def finite_float(value: Any) -> float | None:
|
||
|
|
try:
|
||
|
|
number = float(value)
|
||
|
|
except (TypeError, ValueError):
|
||
|
|
return None
|
||
|
|
return number if math.isfinite(number) else None
|
||
|
|
|
||
|
|
|
||
|
|
def array_stats(array: Any) -> dict[str, Any]:
|
||
|
|
arr = np.asarray(array)
|
||
|
|
out: dict[str, Any] = {
|
||
|
|
"shape": array_shape(arr),
|
||
|
|
"dtype": str(arr.dtype),
|
||
|
|
"size": int(arr.size),
|
||
|
|
}
|
||
|
|
if arr.size == 0 or not np.issubdtype(arr.dtype, np.number):
|
||
|
|
return out
|
||
|
|
|
||
|
|
finite = np.isfinite(arr)
|
||
|
|
out["finite_count"] = int(np.count_nonzero(finite))
|
||
|
|
out["nonfinite_count"] = int(arr.size - out["finite_count"])
|
||
|
|
if out["finite_count"]:
|
||
|
|
finite_values = arr[finite]
|
||
|
|
out.update(
|
||
|
|
{
|
||
|
|
"min": finite_float(np.min(finite_values)),
|
||
|
|
"max": finite_float(np.max(finite_values)),
|
||
|
|
"mean": finite_float(np.mean(finite_values)),
|
||
|
|
}
|
||
|
|
)
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def value_at(array: np.ndarray, index: tuple[int, ...]) -> Any:
|
||
|
|
value = array[index]
|
||
|
|
if isinstance(value, np.generic):
|
||
|
|
return json_ready(value.item())
|
||
|
|
if isinstance(value, np.ndarray):
|
||
|
|
return json_ready(value.tolist())
|
||
|
|
return json_ready(value)
|
||
|
|
|
||
|
|
|
||
|
|
def entity_value_at(array: np.ndarray, entity_index: int | None) -> Any:
|
||
|
|
if entity_index is None or array.ndim == 0:
|
||
|
|
return None
|
||
|
|
value = array[entity_index]
|
||
|
|
if isinstance(value, np.generic):
|
||
|
|
return json_ready(value.item())
|
||
|
|
if isinstance(value, np.ndarray):
|
||
|
|
return json_ready(value.tolist())
|
||
|
|
return json_ready(value)
|
||
|
|
|
||
|
|
|
||
|
|
def update_hash_text(digest: "hashlib._Hash", value: str) -> None:
|
||
|
|
encoded = value.encode("utf-8")
|
||
|
|
digest.update(len(encoded).to_bytes(8, "little"))
|
||
|
|
digest.update(encoded)
|
||
|
|
|
||
|
|
|
||
|
|
def update_hash_array(digest: "hashlib._Hash", label: str, array: Any) -> None:
|
||
|
|
arr = np.ascontiguousarray(np.asarray(array))
|
||
|
|
update_hash_text(digest, label)
|
||
|
|
update_hash_text(digest, str(arr.dtype))
|
||
|
|
update_hash_text(digest, repr(tuple(int(dim) for dim in arr.shape)))
|
||
|
|
digest.update(arr.tobytes())
|
||
|
|
|
||
|
|
|
||
|
|
def source_summary(source: Any) -> dict[str, Any]:
|
||
|
|
return {
|
||
|
|
"file": getattr(source, "file", ""),
|
||
|
|
"function": getattr(source, "function", ""),
|
||
|
|
"lines": json_ready(getattr(source, "lines", None)),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def boundary_field_summary(patch: Any) -> dict[str, Any]:
|
||
|
|
return {
|
||
|
|
"type": getattr(patch, "type", ""),
|
||
|
|
"values_shape": array_shape(getattr(patch, "values", None)),
|
||
|
|
"fixes_value": bool(getattr(patch, "fixes_value", False)),
|
||
|
|
"assignable": bool(getattr(patch, "assignable", False)),
|
||
|
|
"coupled": bool(getattr(patch, "coupled", False)),
|
||
|
|
"updated": bool(getattr(patch, "updated", False)),
|
||
|
|
"patch_internal_shape": array_shape(getattr(patch, "patch_internal", None)),
|
||
|
|
"value_internal_coeffs_shape": array_shape(getattr(patch, "value_internal_coeffs", None)),
|
||
|
|
"value_boundary_coeffs_shape": array_shape(getattr(patch, "value_boundary_coeffs", None)),
|
||
|
|
"gradient_internal_coeffs_shape": array_shape(getattr(patch, "gradient_internal_coeffs", None)),
|
||
|
|
"gradient_boundary_coeffs_shape": array_shape(getattr(patch, "gradient_boundary_coeffs", None)),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def field_summary(field: Any) -> dict[str, Any]:
|
||
|
|
boundary = getattr(field, "boundary", {})
|
||
|
|
return {
|
||
|
|
"name": getattr(field, "name", ""),
|
||
|
|
"kind": getattr(field, "kind", ""),
|
||
|
|
"dimensions": getattr(field, "dimensions", ""),
|
||
|
|
"entity_kind": getattr(field, "entity_kind", ""),
|
||
|
|
"entity_count": int(getattr(field, "entity_count", 0)),
|
||
|
|
"internal": array_stats(getattr(field, "internal")),
|
||
|
|
"boundary": {name: boundary_field_summary(patch) for name, patch in boundary.items()},
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def solve_summary(performance: Any) -> dict[str, Any]:
|
||
|
|
return {
|
||
|
|
"solver_name": getattr(performance, "solver_name", ""),
|
||
|
|
"field_name": getattr(performance, "field_name", ""),
|
||
|
|
"initial_residual": json_ready(getattr(performance, "initial_residual", None)),
|
||
|
|
"final_residual": json_ready(getattr(performance, "final_residual", None)),
|
||
|
|
"n_iterations": json_ready(getattr(performance, "n_iterations", None)),
|
||
|
|
"converged": bool(getattr(performance, "converged", False)),
|
||
|
|
"singular": bool(getattr(performance, "singular", False)),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def matrix_summary(matrix: Any) -> dict[str, Any]:
|
||
|
|
derived: dict[str, Any] = {}
|
||
|
|
for name in ("A", "H", "H1", "flux", "face_flux_correction"):
|
||
|
|
value = getattr(matrix, name, None)
|
||
|
|
if callable(value):
|
||
|
|
value = value()
|
||
|
|
if value is not None:
|
||
|
|
derived[name] = field_summary(value)
|
||
|
|
|
||
|
|
for name in ("residual", "D", "DD"):
|
||
|
|
value = getattr(matrix, name, None)
|
||
|
|
if callable(value):
|
||
|
|
value = value()
|
||
|
|
if value is not None:
|
||
|
|
derived[name] = array_stats(value)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"name": getattr(matrix, "name", ""),
|
||
|
|
"field_name": getattr(matrix, "field_name", ""),
|
||
|
|
"value_rank": getattr(matrix, "value_rank", ""),
|
||
|
|
"dimensions": getattr(matrix, "dimensions", ""),
|
||
|
|
"has_diag": bool(getattr(matrix, "has_diag", False)),
|
||
|
|
"has_upper": bool(getattr(matrix, "has_upper", False)),
|
||
|
|
"has_lower": bool(getattr(matrix, "has_lower", False)),
|
||
|
|
"diagonal": bool(getattr(matrix, "diagonal", False)),
|
||
|
|
"symmetric": bool(getattr(matrix, "symmetric", False)),
|
||
|
|
"asymmetric": bool(getattr(matrix, "asymmetric", False)),
|
||
|
|
"diag": array_stats(getattr(matrix, "diag")),
|
||
|
|
"upper": None if getattr(matrix, "upper", None) is None else array_stats(getattr(matrix, "upper")),
|
||
|
|
"lower": None if getattr(matrix, "lower", None) is None else array_stats(getattr(matrix, "lower")),
|
||
|
|
"source": array_stats(getattr(matrix, "source")),
|
||
|
|
"psi": field_summary(getattr(matrix, "psi")),
|
||
|
|
"internal_coeff_shapes": [array_shape(item) for item in getattr(matrix, "internal_coeffs", [])],
|
||
|
|
"boundary_coeff_shapes": [array_shape(item) for item in getattr(matrix, "boundary_coeffs", [])],
|
||
|
|
"derived": derived,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def summarize_value(value: Any) -> Any:
|
||
|
|
if hasattr(value, "diag") and hasattr(value, "field_name") and hasattr(value, "source"):
|
||
|
|
return matrix_summary(value)
|
||
|
|
if hasattr(value, "internal") and hasattr(value, "entity_kind") and hasattr(value, "boundary"):
|
||
|
|
return field_summary(value)
|
||
|
|
if hasattr(value, "solver_name") and hasattr(value, "initial_residual"):
|
||
|
|
return solve_summary(value)
|
||
|
|
if hasattr(value, "name") and hasattr(value, "phase") and hasattr(value, "outputs"):
|
||
|
|
return transform_summary(value)
|
||
|
|
if isinstance(value, Mapping):
|
||
|
|
return {str(key): summarize_value(item) for key, item in value.items()}
|
||
|
|
if isinstance(value, list):
|
||
|
|
return [summarize_value(item) for item in value]
|
||
|
|
if isinstance(value, tuple):
|
||
|
|
return [summarize_value(item) for item in value]
|
||
|
|
return json_ready(value)
|
||
|
|
|
||
|
|
|
||
|
|
def transform_summary(result: Any) -> dict[str, Any]:
|
||
|
|
return {
|
||
|
|
"name": getattr(result, "name", ""),
|
||
|
|
"phase": getattr(result, "phase", ""),
|
||
|
|
"changed_fields": json_ready(getattr(result, "changed_fields", [])),
|
||
|
|
"source": source_summary(getattr(result, "source", None)),
|
||
|
|
"metadata": json_ready(getattr(result, "metadata", {})),
|
||
|
|
"outputs": summarize_value(getattr(result, "outputs", {})),
|
||
|
|
}
|
||
|
|
|
||
|
|
def graph_stage_summaries(result: Any) -> list[dict[str, Any]]:
|
||
|
|
return [transform_summary(entry) for entry in getattr(result, "outputs", {}).get("graph", [])]
|
||
|
|
|
||
|
|
|
||
|
|
def stage_observability_report(mode: str, stages: list[dict[str, Any]], *, evidence: Mapping[str, Any]) -> dict[str, Any]:
|
||
|
|
stage_by_name = {stage["name"]: stage for stage in stages}
|
||
|
|
failures: list[dict[str, Any]] = []
|
||
|
|
groups = []
|
||
|
|
for group in STAGE_OBSERVABILITY_GROUPS:
|
||
|
|
group_name = group["name"]
|
||
|
|
required_stages = tuple(group[f"{mode}_stages"])
|
||
|
|
missing_stages = [name for name in required_stages if name not in stage_by_name]
|
||
|
|
output_requirements = group.get(f"{mode}_outputs", {})
|
||
|
|
output_checks = {}
|
||
|
|
for stage_name, required_outputs in output_requirements.items():
|
||
|
|
stage = stage_by_name.get(stage_name)
|
||
|
|
if stage is None:
|
||
|
|
continue
|
||
|
|
outputs = stage.get("outputs", {})
|
||
|
|
output_keys = set(outputs) if isinstance(outputs, Mapping) else set()
|
||
|
|
missing_outputs = [name for name in required_outputs if name not in output_keys]
|
||
|
|
output_checks[stage_name] = {
|
||
|
|
"required": list(required_outputs),
|
||
|
|
"available": sorted(output_keys),
|
||
|
|
"missing": missing_outputs,
|
||
|
|
}
|
||
|
|
if missing_outputs:
|
||
|
|
failures.append(
|
||
|
|
{
|
||
|
|
"path": f"stage_observability.{mode}.{group_name}.{stage_name}.outputs",
|
||
|
|
"message": "missing inspectable stage output",
|
||
|
|
"expected": list(required_outputs),
|
||
|
|
"actual": sorted(output_keys),
|
||
|
|
}
|
||
|
|
)
|
||
|
|
if missing_stages:
|
||
|
|
failures.append(
|
||
|
|
{
|
||
|
|
"path": f"stage_observability.{mode}.{group_name}.stages",
|
||
|
|
"message": "missing required solver stage",
|
||
|
|
"expected": list(required_stages),
|
||
|
|
"actual": list(stage_by_name),
|
||
|
|
}
|
||
|
|
)
|
||
|
|
groups.append(
|
||
|
|
{
|
||
|
|
"name": group_name,
|
||
|
|
"required_stages": list(required_stages),
|
||
|
|
"observed": not missing_stages and not any(check["missing"] for check in output_checks.values()),
|
||
|
|
"missing_stages": missing_stages,
|
||
|
|
"output_checks": output_checks,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
if failures:
|
||
|
|
raise BackendExecutionError(
|
||
|
|
f"{mode}_stage_observability",
|
||
|
|
f"{mode} GPU solver-stage observability is incomplete",
|
||
|
|
details={"failure_kind": GPU_STAGE_UNSUPPORTED, "failures": failures},
|
||
|
|
)
|
||
|
|
return {
|
||
|
|
"mode": mode,
|
||
|
|
"graph": [stage["name"] for stage in stages],
|
||
|
|
"groups": groups,
|
||
|
|
"evidence": json_ready(evidence),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def field_dict_to_mapping(fields: Any) -> dict[str, Any]:
|
||
|
|
if isinstance(fields, Mapping):
|
||
|
|
return dict(fields)
|
||
|
|
if hasattr(fields, "fields") and isinstance(fields.fields, Mapping):
|
||
|
|
return dict(fields.fields)
|
||
|
|
return {name: getattr(fields, name) for name in REQUIRED_FIELDS if hasattr(fields, name)}
|
||
|
|
|
||
|
|
|
||
|
|
def validation_details(exc: BaseException) -> dict[str, Any]:
|
||
|
|
if hasattr(exc, "to_dict"):
|
||
|
|
return json_ready(exc.to_dict())
|
||
|
|
return {"type": type(exc).__name__, "message": str(exc)}
|
||
|
|
|
||
|
|
|
||
|
|
def read_fields(stepper: Any, label: str) -> dict[str, Any]:
|
||
|
|
try:
|
||
|
|
return field_dict_to_mapping(stepper.fields())
|
||
|
|
except Exception as exc:
|
||
|
|
raise BackendExecutionError(
|
||
|
|
f"read_{label}_fields",
|
||
|
|
f"failed to read {label} fields for GPU backend",
|
||
|
|
details={"case": getattr(stepper, "case_path", ""), "cause": validation_details(exc)},
|
||
|
|
) from exc
|
||
|
|
|
||
|
|
|
||
|
|
def export_solver_state_checked(foam: Any, stepper: Any, label: str, fields: Mapping[str, Any] | None = None) -> dict[str, Any]:
|
||
|
|
try:
|
||
|
|
field_source = fields if fields is not None else stepper.fields()
|
||
|
|
return foam.export_solver_state(stepper.mesh(), field_source, required_fields=REQUIRED_FIELDS)
|
||
|
|
except Exception as exc:
|
||
|
|
raise BackendExecutionError(
|
||
|
|
f"export_{label}_state",
|
||
|
|
f"failed to export explicit {label} solver state for GPU backend",
|
||
|
|
details=validation_details(exc),
|
||
|
|
) from exc
|
||
|
|
|
||
|
|
|
||
|
|
def visible_turbulence_fields(fields: Mapping[str, Any]) -> list[str]:
|
||
|
|
return [name for name in TURBULENCE_FIELDS if name in fields]
|
||
|
|
|
||
|
|
def gpu_device_present() -> bool:
|
||
|
|
return any(Path(path).exists() for path in ("/dev/nvidia0", "/dev/dri/renderD128"))
|
||
|
|
|
||
|
|
|
||
|
|
def nvidia_device_identity() -> dict[str, Any]:
|
||
|
|
try:
|
||
|
|
completed = subprocess.run(
|
||
|
|
["nvidia-smi", "--query-gpu=name,uuid", "--format=csv,noheader"],
|
||
|
|
check=True,
|
||
|
|
stdout=subprocess.PIPE,
|
||
|
|
stderr=subprocess.PIPE,
|
||
|
|
text=True,
|
||
|
|
timeout=10,
|
||
|
|
)
|
||
|
|
except (FileNotFoundError, subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
||
|
|
return {"device_name": None, "device_uuid": None}
|
||
|
|
|
||
|
|
first = completed.stdout.strip().splitlines()[0] if completed.stdout.strip() else ""
|
||
|
|
if not first:
|
||
|
|
return {"device_name": None, "device_uuid": None}
|
||
|
|
parts = [part.strip() for part in first.split(",", 1)]
|
||
|
|
return {"device_name": parts[0], "device_uuid": parts[1] if len(parts) > 1 else None}
|
||
|
|
|
||
|
|
|
||
|
|
def gpu_stage_contracts() -> dict[str, dict[str, Any]]:
|
||
|
|
contracts: dict[str, dict[str, Any]] = {}
|
||
|
|
for group in STAGE_OBSERVABILITY_GROUPS:
|
||
|
|
name = str(group["name"])
|
||
|
|
split_outputs = {
|
||
|
|
str(stage): list(outputs)
|
||
|
|
for stage, outputs in group.get("split_outputs", {}).items()
|
||
|
|
}
|
||
|
|
field_coverage = sorted(
|
||
|
|
{
|
||
|
|
field
|
||
|
|
for outputs in split_outputs.values()
|
||
|
|
for field in outputs
|
||
|
|
if field in REQUIRED_FIELDS
|
||
|
|
}
|
||
|
|
)
|
||
|
|
kernel_entrypoints = list(GPU_STAGE_KERNEL_ENTRYPOINTS.get(name, ()))
|
||
|
|
contracts[name] = {
|
||
|
|
"required": True,
|
||
|
|
"supported": True,
|
||
|
|
"status": "kernels_registered" if kernel_entrypoints else "kernels_missing",
|
||
|
|
"run_one_stages": list(group.get("run_one_stages", ())),
|
||
|
|
"split_stages": list(group.get("split_stages", ())),
|
||
|
|
"expected_outputs": split_outputs,
|
||
|
|
"field_coverage": field_coverage,
|
||
|
|
"kernel_entrypoints": kernel_entrypoints,
|
||
|
|
"failure_kind_if_missing": None if kernel_entrypoints else GPU_KERNELS_MISSING,
|
||
|
|
}
|
||
|
|
return contracts
|
||
|
|
|
||
|
|
|
||
|
|
def gpu_backend_failure_taxonomy() -> dict[str, Any]:
|
||
|
|
return {
|
||
|
|
GPU_RUNTIME_UNAVAILABLE: {
|
||
|
|
"category": BACKEND_FAILURE,
|
||
|
|
"step": "select_backend",
|
||
|
|
"meaning": "CUDA/Quadrants could not provide a non-host GPU runtime.",
|
||
|
|
},
|
||
|
|
GPU_KERNELS_MISSING: {
|
||
|
|
"category": BACKEND_FAILURE,
|
||
|
|
"step": "execute_gpu_solver_contract",
|
||
|
|
"meaning": "The GPU backend was selected, but required solver-stage kernels are not registered.",
|
||
|
|
},
|
||
|
|
GPU_STAGE_UNSUPPORTED: {
|
||
|
|
"category": BACKEND_FAILURE,
|
||
|
|
"step": "execute_gpu_solver_contract",
|
||
|
|
"meaning": "The GPU backend explicitly cannot execute one or more required RANS solver stages.",
|
||
|
|
},
|
||
|
|
GPU_NUMERICAL_MISMATCH: {
|
||
|
|
"category": COMPARISON_FAILURE,
|
||
|
|
"step": "compare_fields",
|
||
|
|
"meaning": "The GPU backend executed, but one or more required fields failed OpenFOAM oracle parity.",
|
||
|
|
"field_attribution": FIELD_COMPARISON_ATTRIBUTION,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
def solver_state_arrays(value: Any, path: str = "") -> Iterable[tuple[str, np.ndarray]]:
|
||
|
|
if isinstance(value, np.ndarray):
|
||
|
|
yield path, value
|
||
|
|
return
|
||
|
|
if isinstance(value, Mapping):
|
||
|
|
for key, item in value.items():
|
||
|
|
item_path = f"{path}.{key}" if path else str(key)
|
||
|
|
yield from solver_state_arrays(item, item_path)
|
||
|
|
return
|
||
|
|
if isinstance(value, (list, tuple)):
|
||
|
|
for index, item in enumerate(value):
|
||
|
|
yield from solver_state_arrays(item, f"{path}[{index}]")
|
||
|
|
|
||
|
|
|
||
|
|
def gpu_transfer_array_dtype(qd: Any, source: np.ndarray, path: str) -> tuple[Any, str, np.ndarray]:
|
||
|
|
if np.issubdtype(source.dtype, np.integer):
|
||
|
|
if source.size:
|
||
|
|
min_index = int(source.min())
|
||
|
|
max_index = int(source.max())
|
||
|
|
else:
|
||
|
|
min_index = 0
|
||
|
|
max_index = -1
|
||
|
|
int32 = np.iinfo(np.int32)
|
||
|
|
if min_index < int32.min or max_index > int32.max:
|
||
|
|
raise BackendExecutionError(
|
||
|
|
"prepare_gpu_solver_inputs",
|
||
|
|
f"{path}: integer values exceed int32 GPU index range",
|
||
|
|
details={
|
||
|
|
"path": path,
|
||
|
|
"source_dtype": str(source.dtype),
|
||
|
|
"min": min_index,
|
||
|
|
"max": max_index,
|
||
|
|
"gpu_dtype": "i32",
|
||
|
|
},
|
||
|
|
)
|
||
|
|
return qd.i32, "i32", source.astype(np.int32, copy=False)
|
||
|
|
|
||
|
|
if np.issubdtype(source.dtype, np.floating):
|
||
|
|
finite = np.isfinite(source)
|
||
|
|
if not bool(np.all(finite)):
|
||
|
|
raise BackendExecutionError(
|
||
|
|
"prepare_gpu_solver_inputs",
|
||
|
|
f"{path}: non-finite values cannot be copied into GPU solver inputs",
|
||
|
|
details={
|
||
|
|
"path": path,
|
||
|
|
"source_dtype": str(source.dtype),
|
||
|
|
"shape": array_shape(source),
|
||
|
|
"nonfinite_count": int(source.size - np.count_nonzero(finite)),
|
||
|
|
},
|
||
|
|
)
|
||
|
|
if source.dtype == np.dtype("float32"):
|
||
|
|
return qd.f32, "f32", source
|
||
|
|
return qd.f64, "f64", source.astype(np.float64, copy=False)
|
||
|
|
|
||
|
|
raise BackendExecutionError(
|
||
|
|
"prepare_gpu_solver_inputs",
|
||
|
|
f"{path}: unsupported GPU input dtype {source.dtype}",
|
||
|
|
details={"path": path, "source_dtype": str(source.dtype), "shape": array_shape(source)},
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def transfer_solver_state_to_gpu(state: Mapping[str, Any]) -> dict[str, Any]:
|
||
|
|
import quadrants as qd
|
||
|
|
|
||
|
|
arrays: dict[str, dict[str, Any]] = {}
|
||
|
|
device_arrays: list[Any] = []
|
||
|
|
transferred_count = 0
|
||
|
|
source_bytes = 0
|
||
|
|
gpu_bytes = 0
|
||
|
|
|
||
|
|
for path, source in solver_state_arrays(state):
|
||
|
|
source_array = np.asarray(source)
|
||
|
|
source_bytes += int(source_array.nbytes)
|
||
|
|
entry: dict[str, Any] = {
|
||
|
|
"path": path,
|
||
|
|
"source_shape": array_shape(source_array),
|
||
|
|
"source_dtype": str(source_array.dtype),
|
||
|
|
"source_contiguous": bool(source_array.flags.c_contiguous),
|
||
|
|
"source_nbytes": int(source_array.nbytes),
|
||
|
|
"stats": array_stats(source_array),
|
||
|
|
}
|
||
|
|
if source_array.size == 0:
|
||
|
|
entry.update(
|
||
|
|
{
|
||
|
|
"status": "empty_array",
|
||
|
|
"transferred": False,
|
||
|
|
"gpu_shape": array_shape(source_array),
|
||
|
|
"reason": "zero-sized OpenFOAM patch array has no device allocation",
|
||
|
|
}
|
||
|
|
)
|
||
|
|
arrays[path] = entry
|
||
|
|
continue
|
||
|
|
|
||
|
|
qd_dtype, gpu_dtype, gpu_source = gpu_transfer_array_dtype(qd, source_array, path)
|
||
|
|
gpu_source = np.ascontiguousarray(gpu_source)
|
||
|
|
gpu_array = qd.ndarray(qd_dtype, shape=gpu_source.shape)
|
||
|
|
gpu_array.from_numpy(gpu_source)
|
||
|
|
device_arrays.append(gpu_array)
|
||
|
|
transferred_count += 1
|
||
|
|
gpu_bytes += int(gpu_source.nbytes)
|
||
|
|
entry.update(
|
||
|
|
{
|
||
|
|
"status": "transferred",
|
||
|
|
"transferred": True,
|
||
|
|
"gpu_shape": array_shape(gpu_source),
|
||
|
|
"gpu_dtype": gpu_dtype,
|
||
|
|
"gpu_nbytes": int(gpu_source.nbytes),
|
||
|
|
"contiguous_for_transfer": True,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
arrays[path] = entry
|
||
|
|
|
||
|
|
qd.sync()
|
||
|
|
return {
|
||
|
|
"framework": "quadrants",
|
||
|
|
"device": "cuda",
|
||
|
|
"array_count": len(arrays),
|
||
|
|
"transferred_count": transferred_count,
|
||
|
|
"empty_array_count": len(arrays) - transferred_count,
|
||
|
|
"source_nbytes": source_bytes,
|
||
|
|
"gpu_nbytes": gpu_bytes,
|
||
|
|
"arrays": arrays,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def gpu_required_field_inputs(state: Mapping[str, Any], transfer: Mapping[str, Any]) -> dict[str, Any]:
|
||
|
|
arrays = transfer.get("arrays") if isinstance(transfer.get("arrays"), Mapping) else {}
|
||
|
|
fields = state.get("fields") if isinstance(state.get("fields"), Mapping) else {}
|
||
|
|
out: dict[str, Any] = {}
|
||
|
|
for name in REQUIRED_FIELDS:
|
||
|
|
field = fields.get(name) if isinstance(fields.get(name), Mapping) else {}
|
||
|
|
boundary = field.get("boundary") if isinstance(field.get("boundary"), Mapping) else {}
|
||
|
|
internal_path = f"fields.{name}.internal"
|
||
|
|
internal = arrays.get(internal_path) if isinstance(arrays.get(internal_path), Mapping) else {}
|
||
|
|
out[name] = {
|
||
|
|
"status": "gpu_represented" if internal.get("transferred") is True else "missing_gpu_representation",
|
||
|
|
"rank": field.get("rank"),
|
||
|
|
"entity_kind": field.get("entity_kind"),
|
||
|
|
"entity_count": field.get("entity_count"),
|
||
|
|
"internal_path": internal_path,
|
||
|
|
"internal": internal,
|
||
|
|
"boundary_patch_count": len(boundary),
|
||
|
|
"boundary": {
|
||
|
|
patch_name: {
|
||
|
|
"type": patch.get("type"),
|
||
|
|
"values": arrays.get(f"fields.{name}.boundary.{patch_name}.values"),
|
||
|
|
"patch_internal": arrays.get(f"fields.{name}.boundary.{patch_name}.patch_internal"),
|
||
|
|
"value_internal_coeffs": arrays.get(f"fields.{name}.boundary.{patch_name}.value_internal_coeffs"),
|
||
|
|
"value_boundary_coeffs": arrays.get(f"fields.{name}.boundary.{patch_name}.value_boundary_coeffs"),
|
||
|
|
"gradient_internal_coeffs": arrays.get(f"fields.{name}.boundary.{patch_name}.gradient_internal_coeffs"),
|
||
|
|
"gradient_boundary_coeffs": arrays.get(f"fields.{name}.boundary.{patch_name}.gradient_boundary_coeffs"),
|
||
|
|
}
|
||
|
|
for patch_name, patch in boundary.items()
|
||
|
|
},
|
||
|
|
}
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def gpu_mesh_input_summary(state: Mapping[str, Any], transfer: Mapping[str, Any]) -> dict[str, Any]:
|
||
|
|
arrays = transfer.get("arrays") if isinstance(transfer.get("arrays"), Mapping) else {}
|
||
|
|
mesh = state.get("mesh") if isinstance(state.get("mesh"), Mapping) else {}
|
||
|
|
sizes = mesh.get("sizes") if isinstance(mesh.get("sizes"), Mapping) else {}
|
||
|
|
connectivity = mesh.get("connectivity") if isinstance(mesh.get("connectivity"), Mapping) else {}
|
||
|
|
owner = np.asarray(connectivity.get("owner", []))
|
||
|
|
neighbour = np.asarray(connectivity.get("neighbour", []))
|
||
|
|
patches = mesh.get("patches") if isinstance(mesh.get("patches"), list) else []
|
||
|
|
owner_range = [int(owner.min()), int(owner.max())] if owner.size else [None, None]
|
||
|
|
neighbour_range = [int(neighbour.min()), int(neighbour.max())] if neighbour.size else [None, None]
|
||
|
|
return {
|
||
|
|
"sizes": json_ready(sizes),
|
||
|
|
"topology_checks": {
|
||
|
|
"owner_shape": array_shape(owner),
|
||
|
|
"neighbour_shape": array_shape(neighbour),
|
||
|
|
"owner_cell_range": owner_range,
|
||
|
|
"neighbour_cell_range": neighbour_range,
|
||
|
|
"owner_neighbour_gpu_dtype": "i32",
|
||
|
|
"owner_neighbour_in_cell_range": bool(
|
||
|
|
owner.size
|
||
|
|
and neighbour.size
|
||
|
|
and min(owner_range[0], neighbour_range[0]) >= 0
|
||
|
|
and max(owner_range[1], neighbour_range[1]) < int(sizes.get("n_cells", 0))
|
||
|
|
),
|
||
|
|
},
|
||
|
|
"connectivity": {
|
||
|
|
"faces_offsets": arrays.get("mesh.connectivity.faces.offsets"),
|
||
|
|
"faces_values": arrays.get("mesh.connectivity.faces.values"),
|
||
|
|
"cells_offsets": arrays.get("mesh.connectivity.cells.offsets"),
|
||
|
|
"cells_values": arrays.get("mesh.connectivity.cells.values"),
|
||
|
|
"owner": arrays.get("mesh.connectivity.owner"),
|
||
|
|
"neighbour": arrays.get("mesh.connectivity.neighbour"),
|
||
|
|
"ldu_lower_addr": arrays.get("mesh.connectivity.ldu.lower_addr"),
|
||
|
|
"ldu_upper_addr": arrays.get("mesh.connectivity.ldu.upper_addr"),
|
||
|
|
},
|
||
|
|
"geometry": {
|
||
|
|
"points": arrays.get("mesh.geometry.points"),
|
||
|
|
"V": arrays.get("mesh.geometry.V"),
|
||
|
|
"C": arrays.get("mesh.geometry.C"),
|
||
|
|
"Cf": arrays.get("mesh.geometry.Cf"),
|
||
|
|
"Sf": arrays.get("mesh.geometry.Sf"),
|
||
|
|
"magSf": arrays.get("mesh.geometry.magSf"),
|
||
|
|
},
|
||
|
|
"patches": [
|
||
|
|
{
|
||
|
|
"name": patch.get("name"),
|
||
|
|
"type": patch.get("type"),
|
||
|
|
"index": patch.get("index"),
|
||
|
|
"start": patch.get("start"),
|
||
|
|
"size": patch.get("size"),
|
||
|
|
"coupled": patch.get("coupled"),
|
||
|
|
"constraint": patch.get("constraint"),
|
||
|
|
"face_cells": arrays.get(f"mesh.patches[{index}].face_cells"),
|
||
|
|
"face_indices": arrays.get(f"mesh.patches[{index}].face_indices"),
|
||
|
|
"Cf": arrays.get(f"mesh.patches[{index}].Cf"),
|
||
|
|
"Sf": arrays.get(f"mesh.patches[{index}].Sf"),
|
||
|
|
"magSf": arrays.get(f"mesh.patches[{index}].magSf"),
|
||
|
|
}
|
||
|
|
for index, patch in enumerate(patches)
|
||
|
|
],
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def gpu_matrix_input_blockers() -> dict[str, Any]:
|
||
|
|
return {
|
||
|
|
"UEqn": {
|
||
|
|
"status": "provided_by_gpu_stage_kernel",
|
||
|
|
"stage_group": "momentum_assembly",
|
||
|
|
"source_export": "gpu_solver_stages.matrices.UEqn",
|
||
|
|
"kernel_entrypoint": "gpu_rans_momentum_assembly",
|
||
|
|
"required_arrays": ["diag", "source", "psi", "H"],
|
||
|
|
},
|
||
|
|
"pEqn": {
|
||
|
|
"status": "provided_by_gpu_stage_kernel",
|
||
|
|
"stage_group": "pressure_assembly",
|
||
|
|
"source_export": "gpu_solver_stages.matrices.pEqn",
|
||
|
|
"kernel_entrypoint": "gpu_rans_pressure_assembly",
|
||
|
|
"required_arrays": ["diag", "source", "psi", "flux"],
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def prepare_gpu_solver_inputs(foam: Any, stepper: Any, case: Path, prepared: Mapping[str, Any], backend: Mapping[str, Any]) -> dict[str, Any]:
|
||
|
|
fields = read_fields(stepper, "gpu_inputs")
|
||
|
|
solver_state = export_solver_state_checked(foam, stepper, "gpu_inputs", fields)
|
||
|
|
try:
|
||
|
|
foam.validate_solver_state(solver_state, required_fields=REQUIRED_FIELDS, turbulence_fields=TURBULENCE_FIELDS)
|
||
|
|
transfer = transfer_solver_state_to_gpu(solver_state)
|
||
|
|
except BackendExecutionError:
|
||
|
|
raise
|
||
|
|
except Exception as exc:
|
||
|
|
raise BackendExecutionError(
|
||
|
|
"prepare_gpu_solver_inputs",
|
||
|
|
"failed to validate or transfer GPU solver input arrays",
|
||
|
|
details=validation_details(exc),
|
||
|
|
) from exc
|
||
|
|
|
||
|
|
prepared_identity = prepared.get("prepared_case_identity") if isinstance(prepared.get("prepared_case_identity"), Mapping) else {}
|
||
|
|
return {
|
||
|
|
"schema_version": GPU_INPUT_SCHEMA_VERSION,
|
||
|
|
"status": "validated_and_transferred",
|
||
|
|
"case": case,
|
||
|
|
"backend": {
|
||
|
|
"selected": backend.get("selected"),
|
||
|
|
"provider": backend.get("provider"),
|
||
|
|
"device": backend.get("device"),
|
||
|
|
"used_cpu_fallback": backend.get("used_cpu_fallback"),
|
||
|
|
},
|
||
|
|
"source": {
|
||
|
|
"state_export": "foam.export_solver_state(stepper.mesh(), stepper.fields(), required_fields=REQUIRED_FIELDS)",
|
||
|
|
"derived_from_case_role": "run_one",
|
||
|
|
"same_prepared_case_as_oracle": bool(
|
||
|
|
prepared_identity.get("oracle", {}).get("matches_prepared")
|
||
|
|
and prepared_identity.get("run_one", {}).get("matches_prepared")
|
||
|
|
),
|
||
|
|
"prepared_case_identity": prepared_identity,
|
||
|
|
},
|
||
|
|
"validation": {
|
||
|
|
"passed": True,
|
||
|
|
"validator": "foam_stepper.validate_solver_state",
|
||
|
|
"required_fields": list(REQUIRED_FIELDS),
|
||
|
|
"turbulence_fields": list(TURBULENCE_FIELDS),
|
||
|
|
"shape_dtype_topology_checked_before_kernel_launch": True,
|
||
|
|
},
|
||
|
|
"state_summary": foam.describe_solver_state(solver_state),
|
||
|
|
"mesh": gpu_mesh_input_summary(solver_state, transfer),
|
||
|
|
"required_fields": gpu_required_field_inputs(solver_state, transfer),
|
||
|
|
"turbulence": {
|
||
|
|
"fields": list(TURBULENCE_FIELDS),
|
||
|
|
"source": "solver_state.turbulence plus required field GPU arrays",
|
||
|
|
"field_inputs": {name: f"required_fields.{name}" for name in TURBULENCE_FIELDS},
|
||
|
|
},
|
||
|
|
"matrix_data": gpu_matrix_input_blockers(),
|
||
|
|
"transfer": transfer,
|
||
|
|
}
|
||
|
|
|
||
|
|
def gpu_f64_array(source: Any, path: str) -> tuple[Any, np.ndarray, dict[str, Any]]:
|
||
|
|
source_array = np.asarray(source)
|
||
|
|
qd_dtype, gpu_dtype, gpu_source = gpu_transfer_array_dtype(qd, source_array, path)
|
||
|
|
if gpu_dtype != "f64":
|
||
|
|
gpu_source = gpu_source.astype(np.float64, copy=False)
|
||
|
|
gpu_dtype = "f64"
|
||
|
|
qd_dtype = qd.f64
|
||
|
|
gpu_source = np.ascontiguousarray(gpu_source)
|
||
|
|
gpu_array = qd.ndarray(qd_dtype, shape=gpu_source.shape)
|
||
|
|
gpu_array.from_numpy(gpu_source)
|
||
|
|
return gpu_array, gpu_source, {
|
||
|
|
"path": path,
|
||
|
|
"source_shape": array_shape(source_array),
|
||
|
|
"source_dtype": str(source_array.dtype),
|
||
|
|
"gpu_shape": array_shape(gpu_source),
|
||
|
|
"gpu_dtype": gpu_dtype,
|
||
|
|
"gpu_nbytes": int(gpu_source.nbytes),
|
||
|
|
"transferred": True,
|
||
|
|
}
|
||
|
|
|
||
|
|
def gpu_i32_array(source: Any, path: str) -> tuple[Any, np.ndarray, dict[str, Any]]:
|
||
|
|
source_array = np.asarray(source)
|
||
|
|
qd_dtype, gpu_dtype, gpu_source = gpu_transfer_array_dtype(qd, source_array, path)
|
||
|
|
if gpu_dtype != "i32":
|
||
|
|
raise BackendExecutionError(
|
||
|
|
"prepare_gpu_solver_inputs",
|
||
|
|
f"{path}: expected integer topology for GPU index array",
|
||
|
|
details={"path": path, "source_dtype": str(source_array.dtype), "gpu_dtype": gpu_dtype},
|
||
|
|
)
|
||
|
|
gpu_source = np.ascontiguousarray(gpu_source)
|
||
|
|
gpu_array = qd.ndarray(qd_dtype, shape=gpu_source.shape)
|
||
|
|
gpu_array.from_numpy(gpu_source)
|
||
|
|
return gpu_array, gpu_source, {
|
||
|
|
"path": path,
|
||
|
|
"source_shape": array_shape(source_array),
|
||
|
|
"source_dtype": str(source_array.dtype),
|
||
|
|
"gpu_shape": array_shape(gpu_source),
|
||
|
|
"gpu_dtype": gpu_dtype,
|
||
|
|
"gpu_nbytes": int(gpu_source.nbytes),
|
||
|
|
"transferred": True,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def gpu_empty_f64(shape: tuple[int, ...], path: str) -> tuple[Any, dict[str, Any]]:
|
||
|
|
gpu_array = qd.ndarray(qd.f64, shape=shape)
|
||
|
|
return gpu_array, {"path": path, "gpu_shape": list(shape), "gpu_dtype": "f64", "allocated": True}
|
||
|
|
|
||
|
|
|
||
|
|
def read_case_laminar_nu(case: Path) -> float:
|
||
|
|
for relative in ("constant/physicalProperties", "constant/transportProperties", "constant/transportProperties.v2112"):
|
||
|
|
path = case / relative
|
||
|
|
if not path.exists():
|
||
|
|
continue
|
||
|
|
for line in path.read_text().splitlines():
|
||
|
|
stripped = line.strip()
|
||
|
|
if not stripped.startswith("nu"):
|
||
|
|
continue
|
||
|
|
parts = stripped.replace(";", " ").split()
|
||
|
|
if len(parts) >= 2 and parts[0] == "nu":
|
||
|
|
try:
|
||
|
|
return float(parts[1])
|
||
|
|
except ValueError:
|
||
|
|
break
|
||
|
|
return DEFAULT_LAMINAR_NU
|
||
|
|
|
||
|
|
|
||
|
|
def gpu_array_output(name: str, array: np.ndarray, *, kernel: str) -> dict[str, Any]:
|
||
|
|
return {
|
||
|
|
"name": name,
|
||
|
|
"kernel_entrypoint": kernel,
|
||
|
|
"shape": array_shape(array),
|
||
|
|
"dtype": str(array.dtype),
|
||
|
|
"stats": array_stats(array),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def gpu_matrix_output(
|
||
|
|
name: str,
|
||
|
|
field_name: str,
|
||
|
|
diag: np.ndarray,
|
||
|
|
source: np.ndarray,
|
||
|
|
psi: np.ndarray,
|
||
|
|
*,
|
||
|
|
kernel: str,
|
||
|
|
upper: np.ndarray | None = None,
|
||
|
|
upper_kernel: str | None = None,
|
||
|
|
lower: np.ndarray | None = None,
|
||
|
|
lower_kernel: str | None = None,
|
||
|
|
) -> dict[str, Any]:
|
||
|
|
out = {
|
||
|
|
"name": name,
|
||
|
|
"field_name": field_name,
|
||
|
|
"value_rank": "vector" if source.ndim == 2 else "scalar",
|
||
|
|
"kernel_entrypoint": kernel,
|
||
|
|
"diag": gpu_array_output(f"{name}.diag", diag, kernel=kernel),
|
||
|
|
"source": gpu_array_output(f"{name}.source", source, kernel=kernel),
|
||
|
|
"psi": gpu_array_output(f"{name}.psi", psi, kernel=kernel),
|
||
|
|
"has_upper": upper is not None,
|
||
|
|
"has_lower": lower is not None,
|
||
|
|
"gpu_backed": True,
|
||
|
|
}
|
||
|
|
if upper is not None:
|
||
|
|
out["upper"] = gpu_array_output(f"{name}.upper", upper, kernel=upper_kernel or kernel)
|
||
|
|
if lower is not None:
|
||
|
|
out["lower"] = gpu_array_output(f"{name}.lower", lower, kernel=lower_kernel or kernel)
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def gpu_stage_result(name: str, outputs: Mapping[str, Any], *, kernels: Iterable[str], changed_fields: Iterable[str] = ()) -> GpuTransformResult:
|
||
|
|
return GpuTransformResult(
|
||
|
|
name=name,
|
||
|
|
phase="gpu_solver_stage",
|
||
|
|
source=GpuSourceLocation(file=__file__, function=name),
|
||
|
|
inputs={},
|
||
|
|
outputs=dict(outputs),
|
||
|
|
changed_fields=list(changed_fields),
|
||
|
|
metadata={
|
||
|
|
"backend": "gpu",
|
||
|
|
"framework": "quadrants",
|
||
|
|
"kernel_entrypoints": list(kernels),
|
||
|
|
"used_cpu_fallback": False,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def clone_gpu_patch_field(patch: Any) -> GpuPatchField:
|
||
|
|
return GpuPatchField(
|
||
|
|
name=getattr(patch, "name", ""),
|
||
|
|
type=getattr(patch, "type", ""),
|
||
|
|
values=np.asarray(getattr(patch, "values")),
|
||
|
|
fixes_value=bool(getattr(patch, "fixes_value", False)),
|
||
|
|
assignable=bool(getattr(patch, "assignable", False)),
|
||
|
|
coupled=bool(getattr(patch, "coupled", False)),
|
||
|
|
updated=bool(getattr(patch, "updated", False)),
|
||
|
|
patch_internal=None if getattr(patch, "patch_internal", None) is None else np.asarray(getattr(patch, "patch_internal")),
|
||
|
|
value_internal_coeffs=None if getattr(patch, "value_internal_coeffs", None) is None else np.asarray(getattr(patch, "value_internal_coeffs")),
|
||
|
|
value_boundary_coeffs=None if getattr(patch, "value_boundary_coeffs", None) is None else np.asarray(getattr(patch, "value_boundary_coeffs")),
|
||
|
|
gradient_internal_coeffs=None if getattr(patch, "gradient_internal_coeffs", None) is None else np.asarray(getattr(patch, "gradient_internal_coeffs")),
|
||
|
|
gradient_boundary_coeffs=None if getattr(patch, "gradient_boundary_coeffs", None) is None else np.asarray(getattr(patch, "gradient_boundary_coeffs")),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def gpu_field_from_source(source: Any, internal: np.ndarray) -> GpuField:
|
||
|
|
boundary = getattr(source, "boundary", {})
|
||
|
|
return GpuField(
|
||
|
|
name=getattr(source, "name", ""),
|
||
|
|
kind=getattr(source, "kind", ""),
|
||
|
|
dimensions=getattr(source, "dimensions", ""),
|
||
|
|
entity_kind=getattr(source, "entity_kind", ""),
|
||
|
|
entity_count=int(getattr(source, "entity_count", np.asarray(internal).shape[0] if np.asarray(internal).ndim else 0)),
|
||
|
|
internal=np.ascontiguousarray(internal),
|
||
|
|
boundary={name: clone_gpu_patch_field(patch) for name, patch in boundary.items()},
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def gpu_solver_stage_report(gpu_run: Mapping[str, Any]) -> dict[str, Any]:
|
||
|
|
return {
|
||
|
|
key: value
|
||
|
|
for key, value in gpu_run.items()
|
||
|
|
if key not in {"field_objects", "stage_objects"}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def quadrants_kernel_evidence(expected: Iterable[str]) -> dict[str, Any]:
|
||
|
|
try:
|
||
|
|
from quadrants.profiler.kernel_profiler import get_default_kernel_profiler
|
||
|
|
|
||
|
|
profiler = get_default_kernel_profiler()
|
||
|
|
profiler._update_records()
|
||
|
|
records = list(profiler._traced_records)
|
||
|
|
generated_kernel_names = sorted({str(record.name) for record in records})
|
||
|
|
return {
|
||
|
|
"available": True,
|
||
|
|
"expected_kernel_entrypoints": list(expected),
|
||
|
|
"generated_kernel_names": generated_kernel_names,
|
||
|
|
"profile_record_count": len(records),
|
||
|
|
"device_time_ms_total": float(sum(record.kernel_time for record in records)),
|
||
|
|
}
|
||
|
|
except Exception as exc:
|
||
|
|
return {
|
||
|
|
"available": False,
|
||
|
|
"expected_kernel_entrypoints": list(expected),
|
||
|
|
"cause": {"type": type(exc).__name__, "message": str(exc)},
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def run_gpu_solver_stage_smoke(stepper: Any, backend: Mapping[str, Any], case: Path) -> dict[str, Any]:
|
||
|
|
fields = read_fields(stepper, "gpu_stage_smoke")
|
||
|
|
gpu_solver_started = time.perf_counter()
|
||
|
|
input_transfer_started = time.perf_counter()
|
||
|
|
u_gpu, u_np, u_transfer = gpu_f64_array(fields["U"].internal, "fields.U.internal")
|
||
|
|
p_gpu, p_np, p_transfer = gpu_f64_array(fields["p"].internal, "fields.p.internal")
|
||
|
|
phi_gpu, phi_np, phi_transfer = gpu_f64_array(fields["phi"].internal, "fields.phi.internal")
|
||
|
|
nut_gpu, nut_np, nut_transfer = gpu_f64_array(fields["nut"].internal, "fields.nut.internal")
|
||
|
|
k_gpu, k_np, k_transfer = gpu_f64_array(fields["k"].internal, "fields.k.internal")
|
||
|
|
omega_gpu, omega_np, omega_transfer = gpu_f64_array(fields["omega"].internal, "fields.omega.internal")
|
||
|
|
n_cells = int(u_np.shape[0])
|
||
|
|
n_internal_faces = int(phi_np.shape[0])
|
||
|
|
mesh = stepper.mesh()
|
||
|
|
laminar_nu = read_case_laminar_nu(case)
|
||
|
|
owner_gpu, owner_np, owner_transfer = gpu_i32_array(np.asarray(mesh.owner)[:n_internal_faces], "mesh.connectivity.owner.internal")
|
||
|
|
neighbour_gpu, neighbour_np, neighbour_transfer = gpu_i32_array(np.asarray(mesh.neighbour)[:n_internal_faces], "mesh.connectivity.neighbour.internal")
|
||
|
|
sf_gpu, sf_np, sf_transfer = gpu_f64_array(np.asarray(mesh.Sf)[:n_internal_faces], "mesh.geometry.Sf.internal")
|
||
|
|
cell_centres_gpu, cell_centres_np, cell_centres_transfer = gpu_f64_array(np.asarray(mesh.C), "mesh.geometry.C")
|
||
|
|
cell_volumes_gpu, cell_volumes_np, cell_volumes_transfer = gpu_f64_array(np.asarray(mesh.V), "mesh.geometry.V")
|
||
|
|
mag_sf_gpu, mag_sf_np, mag_sf_transfer = gpu_f64_array(np.asarray(mesh.magSf)[:n_internal_faces], "mesh.geometry.magSf.internal")
|
||
|
|
boundary_face_cells_parts: list[np.ndarray] = []
|
||
|
|
boundary_phi_parts: list[np.ndarray] = []
|
||
|
|
for patch in mesh.boundary:
|
||
|
|
patch_phi = fields["phi"].boundary.get(patch.name)
|
||
|
|
if patch_phi is None:
|
||
|
|
continue
|
||
|
|
values = np.asarray(patch_phi.values, dtype=np.float64).reshape(-1)
|
||
|
|
if values.size == 0:
|
||
|
|
continue
|
||
|
|
face_cells = np.asarray(patch.face_cells, dtype=np.int32).reshape(-1)
|
||
|
|
if face_cells.shape[0] != values.shape[0]:
|
||
|
|
raise BackendExecutionError(
|
||
|
|
"prepare_boundary_pressure_source",
|
||
|
|
"boundary phi and boundary face-cell arrays have different lengths",
|
||
|
|
details={"patch": patch.name, "phi_size": int(values.shape[0]), "face_cell_size": int(face_cells.shape[0])},
|
||
|
|
)
|
||
|
|
boundary_face_cells_parts.append(face_cells)
|
||
|
|
boundary_phi_parts.append(values)
|
||
|
|
boundary_face_cells_np = np.concatenate(boundary_face_cells_parts) if boundary_face_cells_parts else np.empty((0,), dtype=np.int32)
|
||
|
|
boundary_phi_np = np.concatenate(boundary_phi_parts) if boundary_phi_parts else np.empty((0,), dtype=np.float64)
|
||
|
|
n_pressure_boundary_faces = int(boundary_phi_np.shape[0])
|
||
|
|
boundary_face_cells_gpu, boundary_face_cells_np, boundary_face_cells_transfer = gpu_i32_array(boundary_face_cells_np, "mesh.boundary.face_cells.pressure_source")
|
||
|
|
boundary_phi_gpu, boundary_phi_np, boundary_phi_transfer = gpu_f64_array(boundary_phi_np, "fields.phi.boundary.pressure_source")
|
||
|
|
pressure_mixed_face_cells_parts: list[np.ndarray] = []
|
||
|
|
pressure_mixed_scale_parts: list[np.ndarray] = []
|
||
|
|
pressure_mixed_value_parts: list[np.ndarray] = []
|
||
|
|
for patch in mesh.boundary:
|
||
|
|
p_patch = fields["p"].boundary.get(patch.name)
|
||
|
|
if p_patch is None or getattr(p_patch, "type", "") != "freestreamPressure":
|
||
|
|
continue
|
||
|
|
u_patch = fields["U"].boundary.get(patch.name)
|
||
|
|
if u_patch is None:
|
||
|
|
raise BackendExecutionError(
|
||
|
|
"prepare_pressure_mixed_boundary_laplacian",
|
||
|
|
"freestreamPressure patch requires matching U patch values",
|
||
|
|
details={"patch": patch.name},
|
||
|
|
)
|
||
|
|
face_cells = np.asarray(patch.face_cells, dtype=np.int32).reshape(-1)
|
||
|
|
values = np.asarray(p_patch.values, dtype=np.float64).reshape(-1)
|
||
|
|
u_values = np.asarray(u_patch.values, dtype=np.float64).reshape(-1, 3)
|
||
|
|
face_centres = np.asarray(patch.Cf, dtype=np.float64)
|
||
|
|
face_area_vectors = np.asarray(patch.Sf, dtype=np.float64)
|
||
|
|
face_area_magnitudes = np.asarray(patch.magSf, dtype=np.float64).reshape(-1)
|
||
|
|
expected_face_shape = (face_cells.shape[0],)
|
||
|
|
expected_vector_shape = (face_cells.shape[0], 3)
|
||
|
|
if values.shape != expected_face_shape or u_values.shape != expected_vector_shape or face_centres.shape != expected_vector_shape or face_area_vectors.shape != expected_vector_shape or face_area_magnitudes.shape != expected_face_shape:
|
||
|
|
raise BackendExecutionError(
|
||
|
|
"prepare_pressure_mixed_boundary_laplacian",
|
||
|
|
"freestreamPressure patch arrays have incompatible shapes",
|
||
|
|
details={
|
||
|
|
"patch": patch.name,
|
||
|
|
"face_cells_shape": list(face_cells.shape),
|
||
|
|
"values_shape": list(values.shape),
|
||
|
|
"U_shape": list(u_values.shape),
|
||
|
|
"Cf_shape": list(face_centres.shape),
|
||
|
|
"Sf_shape": list(face_area_vectors.shape),
|
||
|
|
"magSf_shape": list(face_area_magnitudes.shape),
|
||
|
|
},
|
||
|
|
)
|
||
|
|
normals = face_area_vectors / np.maximum(face_area_magnitudes[:, None], 1.0e-300)
|
||
|
|
velocity_magnitudes = np.linalg.norm(u_values, axis=1)
|
||
|
|
normal_velocity = np.sum(u_values * normals, axis=1)
|
||
|
|
value_fraction = np.where(velocity_magnitudes > 1.0e-300, 0.5 + 0.5 * normal_velocity / velocity_magnitudes, 0.5)
|
||
|
|
deltas = face_centres - cell_centres_np[face_cells]
|
||
|
|
projected_delta = np.sum(normals * deltas, axis=1)
|
||
|
|
delta_magnitudes = np.linalg.norm(deltas, axis=1)
|
||
|
|
delta_coefficients = 1.0 / np.maximum(projected_delta, 0.05 * delta_magnitudes)
|
||
|
|
pressure_mixed_face_cells_parts.append(face_cells)
|
||
|
|
pressure_mixed_scale_parts.append(value_fraction * face_area_magnitudes * delta_coefficients)
|
||
|
|
pressure_mixed_value_parts.append(values)
|
||
|
|
pressure_mixed_face_cells_np = np.concatenate(pressure_mixed_face_cells_parts) if pressure_mixed_face_cells_parts else np.empty((0,), dtype=np.int32)
|
||
|
|
pressure_mixed_scales_np = np.concatenate(pressure_mixed_scale_parts) if pressure_mixed_scale_parts else np.empty((0,), dtype=np.float64)
|
||
|
|
pressure_mixed_values_np = np.concatenate(pressure_mixed_value_parts) if pressure_mixed_value_parts else np.empty((0,), dtype=np.float64)
|
||
|
|
n_pressure_mixed_faces = int(pressure_mixed_face_cells_np.shape[0])
|
||
|
|
pressure_mixed_face_cells_gpu, pressure_mixed_face_cells_np, pressure_mixed_face_cells_transfer = gpu_i32_array(pressure_mixed_face_cells_np, "mesh.boundary.face_cells.pressure_mixed_laplacian")
|
||
|
|
pressure_mixed_scales_gpu, pressure_mixed_scales_np, pressure_mixed_scales_transfer = gpu_f64_array(pressure_mixed_scales_np, "mesh.boundary.scale.pressure_mixed_laplacian")
|
||
|
|
pressure_mixed_values_gpu, pressure_mixed_values_np, pressure_mixed_values_transfer = gpu_f64_array(pressure_mixed_values_np, "fields.p.boundary.pressure_mixed_laplacian")
|
||
|
|
hbyA_constraint_face_cells_parts: list[np.ndarray] = []
|
||
|
|
hbyA_constraint_values_parts: list[np.ndarray] = []
|
||
|
|
for patch in mesh.boundary:
|
||
|
|
u_patch = fields["U"].boundary.get(patch.name)
|
||
|
|
if u_patch is None or getattr(u_patch, "assignable", True) or getattr(u_patch, "type", "") != "freestreamVelocity":
|
||
|
|
continue
|
||
|
|
values = np.asarray(u_patch.values, dtype=np.float64).reshape(-1, 3)
|
||
|
|
if values.shape[0] == 0:
|
||
|
|
continue
|
||
|
|
face_cells = np.asarray(patch.face_cells, dtype=np.int32).reshape(-1)
|
||
|
|
if face_cells.shape[0] != values.shape[0]:
|
||
|
|
raise BackendExecutionError(
|
||
|
|
"prepare_HbyA_boundary_constraint",
|
||
|
|
"velocity boundary values and face-cell arrays have different lengths",
|
||
|
|
details={"patch": patch.name, "U_size": int(values.shape[0]), "face_cell_size": int(face_cells.shape[0])},
|
||
|
|
)
|
||
|
|
hbyA_constraint_face_cells_parts.append(face_cells)
|
||
|
|
hbyA_constraint_values_parts.append(values)
|
||
|
|
hbyA_constraint_face_cells_np = np.concatenate(hbyA_constraint_face_cells_parts) if hbyA_constraint_face_cells_parts else np.empty((0,), dtype=np.int32)
|
||
|
|
hbyA_constraint_values_np = np.concatenate(hbyA_constraint_values_parts) if hbyA_constraint_values_parts else np.empty((0, 3), dtype=np.float64)
|
||
|
|
n_hbyA_constraint_faces = int(hbyA_constraint_face_cells_np.shape[0])
|
||
|
|
hbyA_constraint_face_cells_gpu, hbyA_constraint_face_cells_np, hbyA_constraint_face_cells_transfer = gpu_i32_array(hbyA_constraint_face_cells_np, "mesh.boundary.face_cells.HbyA_constraint")
|
||
|
|
hbyA_constraint_values_gpu, hbyA_constraint_values_np, hbyA_constraint_values_transfer = gpu_f64_array(hbyA_constraint_values_np, "fields.U.boundary.HbyA_constraint")
|
||
|
|
momentum_wall_face_cells_parts: list[np.ndarray] = []
|
||
|
|
momentum_wall_values_parts: list[np.ndarray] = []
|
||
|
|
momentum_wall_nut_parts: list[np.ndarray] = []
|
||
|
|
momentum_wall_face_centres_parts: list[np.ndarray] = []
|
||
|
|
momentum_wall_area_vectors_parts: list[np.ndarray] = []
|
||
|
|
momentum_wall_area_magnitudes_parts: list[np.ndarray] = []
|
||
|
|
for patch in mesh.boundary:
|
||
|
|
u_patch = fields["U"].boundary.get(patch.name)
|
||
|
|
if u_patch is None or getattr(u_patch, "type", "") != "noSlip":
|
||
|
|
continue
|
||
|
|
values = np.asarray(u_patch.values, dtype=np.float64).reshape(-1, 3)
|
||
|
|
if values.shape[0] == 0:
|
||
|
|
continue
|
||
|
|
face_cells = np.asarray(patch.face_cells, dtype=np.int32).reshape(-1)
|
||
|
|
face_centres = np.asarray(patch.Cf, dtype=np.float64)
|
||
|
|
face_area_vectors = np.asarray(patch.Sf, dtype=np.float64)
|
||
|
|
face_area_magnitudes = np.asarray(patch.magSf, dtype=np.float64).reshape(-1)
|
||
|
|
expected_wall_shape = (face_cells.shape[0], 3)
|
||
|
|
if values.shape != expected_wall_shape or face_centres.shape != expected_wall_shape or face_area_vectors.shape != expected_wall_shape or face_area_magnitudes.shape[0] != face_cells.shape[0]:
|
||
|
|
raise BackendExecutionError(
|
||
|
|
"prepare_momentum_wall_diffusion",
|
||
|
|
"wall velocity patch arrays have incompatible shapes",
|
||
|
|
details={
|
||
|
|
"patch": patch.name,
|
||
|
|
"face_cells_shape": list(face_cells.shape),
|
||
|
|
"values_shape": list(values.shape),
|
||
|
|
"face_centres_shape": list(face_centres.shape),
|
||
|
|
"face_area_vectors_shape": list(face_area_vectors.shape),
|
||
|
|
"face_area_magnitudes_shape": list(face_area_magnitudes.shape),
|
||
|
|
},
|
||
|
|
)
|
||
|
|
nut_patch = fields["nut"].boundary.get(patch.name)
|
||
|
|
if nut_patch is None:
|
||
|
|
nut_values = np.zeros((face_cells.shape[0],), dtype=np.float64)
|
||
|
|
else:
|
||
|
|
nut_values = np.asarray(nut_patch.values, dtype=np.float64).reshape(-1)
|
||
|
|
if nut_values.shape[0] != face_cells.shape[0]:
|
||
|
|
raise BackendExecutionError(
|
||
|
|
"prepare_momentum_wall_diffusion",
|
||
|
|
"wall nut patch values and face-cell arrays have different lengths",
|
||
|
|
details={"patch": patch.name, "nut_size": int(nut_values.shape[0]), "face_cell_size": int(face_cells.shape[0])},
|
||
|
|
)
|
||
|
|
momentum_wall_face_cells_parts.append(face_cells)
|
||
|
|
momentum_wall_values_parts.append(values)
|
||
|
|
momentum_wall_nut_parts.append(nut_values)
|
||
|
|
momentum_wall_face_centres_parts.append(face_centres)
|
||
|
|
momentum_wall_area_vectors_parts.append(face_area_vectors)
|
||
|
|
momentum_wall_area_magnitudes_parts.append(face_area_magnitudes)
|
||
|
|
momentum_wall_face_cells_np = np.concatenate(momentum_wall_face_cells_parts) if momentum_wall_face_cells_parts else np.empty((0,), dtype=np.int32)
|
||
|
|
momentum_wall_values_np = np.concatenate(momentum_wall_values_parts) if momentum_wall_values_parts else np.empty((0, 3), dtype=np.float64)
|
||
|
|
momentum_wall_nut_np = np.concatenate(momentum_wall_nut_parts) if momentum_wall_nut_parts else np.empty((0,), dtype=np.float64)
|
||
|
|
momentum_wall_face_centres_np = np.concatenate(momentum_wall_face_centres_parts) if momentum_wall_face_centres_parts else np.empty((0, 3), dtype=np.float64)
|
||
|
|
momentum_wall_area_vectors_np = np.concatenate(momentum_wall_area_vectors_parts) if momentum_wall_area_vectors_parts else np.empty((0, 3), dtype=np.float64)
|
||
|
|
momentum_wall_area_magnitudes_np = np.concatenate(momentum_wall_area_magnitudes_parts) if momentum_wall_area_magnitudes_parts else np.empty((0,), dtype=np.float64)
|
||
|
|
n_momentum_wall_faces = int(momentum_wall_face_cells_np.shape[0])
|
||
|
|
momentum_wall_face_cells_gpu, momentum_wall_face_cells_np, momentum_wall_face_cells_transfer = gpu_i32_array(momentum_wall_face_cells_np, "mesh.boundary.face_cells.momentum_wall_diffusion")
|
||
|
|
momentum_wall_values_gpu, momentum_wall_values_np, momentum_wall_values_transfer = gpu_f64_array(momentum_wall_values_np, "fields.U.boundary.momentum_wall_diffusion")
|
||
|
|
momentum_wall_nut_gpu, momentum_wall_nut_np, momentum_wall_nut_transfer = gpu_f64_array(momentum_wall_nut_np, "fields.nut.boundary.momentum_wall_diffusion")
|
||
|
|
momentum_wall_face_centres_gpu, momentum_wall_face_centres_np, momentum_wall_face_centres_transfer = gpu_f64_array(momentum_wall_face_centres_np, "mesh.boundary.Cf.momentum_wall_diffusion")
|
||
|
|
momentum_wall_area_vectors_gpu, momentum_wall_area_vectors_np, momentum_wall_area_vectors_transfer = gpu_f64_array(momentum_wall_area_vectors_np, "mesh.boundary.Sf.momentum_wall_diffusion")
|
||
|
|
momentum_wall_area_magnitudes_gpu, momentum_wall_area_magnitudes_np, momentum_wall_area_magnitudes_transfer = gpu_f64_array(momentum_wall_area_magnitudes_np, "mesh.boundary.magSf.momentum_wall_diffusion")
|
||
|
|
omega_wall_distances_by_cell = np.full((n_cells,), np.inf, dtype=np.float64)
|
||
|
|
omega_wall_seed_count = 0
|
||
|
|
for patch in mesh.boundary:
|
||
|
|
omega_patch = fields["omega"].boundary.get(patch.name)
|
||
|
|
if omega_patch is None or "omegaWallFunction" not in omega_patch.type:
|
||
|
|
continue
|
||
|
|
face_cells = np.asarray(patch.face_cells, dtype=np.int32).reshape(-1)
|
||
|
|
face_centres = np.asarray(patch.Cf, dtype=np.float64)
|
||
|
|
face_area_vectors = np.asarray(patch.Sf, dtype=np.float64)
|
||
|
|
expected_wall_shape = (face_cells.shape[0], 3)
|
||
|
|
if face_centres.shape != expected_wall_shape or face_area_vectors.shape != expected_wall_shape:
|
||
|
|
raise BackendExecutionError(
|
||
|
|
"prepare_omega_wall_update",
|
||
|
|
"omega wall patch geometry and face-cell arrays have incompatible shapes",
|
||
|
|
details={
|
||
|
|
"patch": patch.name,
|
||
|
|
"face_cells_shape": list(face_cells.shape),
|
||
|
|
"face_centres_shape": list(face_centres.shape),
|
||
|
|
"face_area_vectors_shape": list(face_area_vectors.shape),
|
||
|
|
},
|
||
|
|
)
|
||
|
|
wall_vectors = cell_centres_np[face_cells] - face_centres
|
||
|
|
face_area_magnitudes = np.linalg.norm(face_area_vectors, axis=1)
|
||
|
|
wall_distances = np.abs(np.sum(wall_vectors * face_area_vectors, axis=1)) / np.maximum(face_area_magnitudes, 1.0e-300)
|
||
|
|
np.minimum.at(omega_wall_distances_by_cell, face_cells, wall_distances)
|
||
|
|
omega_wall_seed_count += int(face_cells.shape[0])
|
||
|
|
omega_wall_layer_count = 1
|
||
|
|
for _ in range(omega_wall_layer_count):
|
||
|
|
propagated_distances = omega_wall_distances_by_cell.copy()
|
||
|
|
cell_centre_delta = np.linalg.norm(cell_centres_np[neighbour_np] - cell_centres_np[owner_np], axis=1)
|
||
|
|
owner_has_wall = np.isfinite(omega_wall_distances_by_cell[owner_np])
|
||
|
|
np.minimum.at(
|
||
|
|
propagated_distances,
|
||
|
|
neighbour_np[owner_has_wall],
|
||
|
|
omega_wall_distances_by_cell[owner_np[owner_has_wall]] + cell_centre_delta[owner_has_wall],
|
||
|
|
)
|
||
|
|
neighbour_has_wall = np.isfinite(omega_wall_distances_by_cell[neighbour_np])
|
||
|
|
np.minimum.at(
|
||
|
|
propagated_distances,
|
||
|
|
owner_np[neighbour_has_wall],
|
||
|
|
omega_wall_distances_by_cell[neighbour_np[neighbour_has_wall]] + cell_centre_delta[neighbour_has_wall],
|
||
|
|
)
|
||
|
|
omega_wall_distances_by_cell = propagated_distances
|
||
|
|
omega_wall_cells_np = np.flatnonzero(np.isfinite(omega_wall_distances_by_cell)).astype(np.int32, copy=False)
|
||
|
|
omega_wall_distances_np = omega_wall_distances_by_cell[omega_wall_cells_np].astype(np.float64, copy=False)
|
||
|
|
n_omega_wall_cells = int(omega_wall_cells_np.shape[0])
|
||
|
|
omega_wall_cells_gpu, omega_wall_cells_np, omega_wall_cells_transfer = gpu_i32_array(omega_wall_cells_np, "mesh.boundary.cells.omega_wall")
|
||
|
|
omega_wall_distances_gpu, omega_wall_distances_np, omega_wall_distances_transfer = gpu_f64_array(omega_wall_distances_np, "mesh.boundary.normal_wall_distance.omega")
|
||
|
|
input_transfer_wall_seconds = time.perf_counter() - input_transfer_started
|
||
|
|
u_diag_gpu, u_diag_alloc = gpu_empty_f64((n_cells,), "gpu_stages.UEqn.diag")
|
||
|
|
u_source_gpu, u_source_alloc = gpu_empty_f64(tuple(u_np.shape), "gpu_stages.UEqn.source")
|
||
|
|
u_upper_gpu, u_upper_alloc = gpu_empty_f64((n_internal_faces,), "gpu_stages.UEqn.upper")
|
||
|
|
u_lower_gpu, u_lower_alloc = gpu_empty_f64((n_internal_faces,), "gpu_stages.UEqn.lower")
|
||
|
|
rAU_gpu, rAU_alloc = gpu_empty_f64((n_cells,), "gpu_stages.pressure_inputs.rAU")
|
||
|
|
rAtU_gpu, rAtU_alloc = gpu_empty_f64((n_cells,), "gpu_stages.pressure_inputs.rAtU")
|
||
|
|
HbyA_gpu, HbyA_alloc = gpu_empty_f64(tuple(u_np.shape), "gpu_stages.pressure_inputs.HbyA")
|
||
|
|
phiHbyA_gpu, phiHbyA_alloc = gpu_empty_f64((n_internal_faces,), "gpu_stages.pressure_inputs.phiHbyA")
|
||
|
|
p_diag_gpu, p_diag_alloc = gpu_empty_f64((n_cells,), "gpu_stages.pEqn.diag")
|
||
|
|
p_source_gpu, p_source_alloc = gpu_empty_f64(tuple(p_np.shape), "gpu_stages.pEqn.source")
|
||
|
|
p_upper_gpu, p_upper_alloc = gpu_empty_f64((n_internal_faces,), "gpu_stages.pEqn.upper")
|
||
|
|
u_solved_gpu, u_solved_alloc = gpu_empty_f64(tuple(u_np.shape), "gpu_stages.solve_UEqn.U")
|
||
|
|
u_residual_gpu, u_residual_alloc = gpu_empty_f64(tuple(u_np.shape), "gpu_stages.solve_UEqn.residual")
|
||
|
|
u_shadow_gpu, u_shadow_alloc = gpu_empty_f64(tuple(u_np.shape), "gpu_stages.solve_UEqn.shadow_residual")
|
||
|
|
u_direction_gpu, u_direction_alloc = gpu_empty_f64(tuple(u_np.shape), "gpu_stages.solve_UEqn.direction")
|
||
|
|
u_operator_direction_gpu, u_operator_direction_alloc = gpu_empty_f64(tuple(u_np.shape), "gpu_stages.solve_UEqn.operator_direction")
|
||
|
|
u_intermediate_gpu, u_intermediate_alloc = gpu_empty_f64(tuple(u_np.shape), "gpu_stages.solve_UEqn.intermediate")
|
||
|
|
u_operator_intermediate_gpu, u_operator_intermediate_alloc = gpu_empty_f64(tuple(u_np.shape), "gpu_stages.solve_UEqn.operator_intermediate")
|
||
|
|
u_rr_gpu, u_rr_alloc = gpu_empty_f64((1,), "gpu_stages.solve_UEqn.residual_squared")
|
||
|
|
u_rho_gpu, u_rho_alloc = gpu_empty_f64((1,), "gpu_stages.solve_UEqn.rho")
|
||
|
|
u_denominator_gpu, u_denominator_alloc = gpu_empty_f64((1,), "gpu_stages.solve_UEqn.denominator")
|
||
|
|
u_omega_numerator_gpu, u_omega_numerator_alloc = gpu_empty_f64((1,), "gpu_stages.solve_UEqn.omega_numerator")
|
||
|
|
u_omega_denominator_gpu, u_omega_denominator_alloc = gpu_empty_f64((1,), "gpu_stages.solve_UEqn.omega_denominator")
|
||
|
|
p_solved_gpu, p_solved_alloc = gpu_empty_f64(tuple(p_np.shape), "gpu_stages.solve_pEqn.p")
|
||
|
|
p_work_gpu, p_work_alloc = gpu_empty_f64(tuple(p_np.shape), "gpu_stages.solve_pEqn.work")
|
||
|
|
p_residual_gpu, p_residual_alloc = gpu_empty_f64(tuple(p_np.shape), "gpu_stages.solve_pEqn.residual")
|
||
|
|
p_direction_gpu, p_direction_alloc = gpu_empty_f64(tuple(p_np.shape), "gpu_stages.solve_pEqn.direction")
|
||
|
|
p_operator_gpu, p_operator_alloc = gpu_empty_f64(tuple(p_np.shape), "gpu_stages.solve_pEqn.operator")
|
||
|
|
p_rr_gpu, p_rr_alloc = gpu_empty_f64((1,), "gpu_stages.solve_pEqn.residual_squared")
|
||
|
|
p_denominator_gpu, p_denominator_alloc = gpu_empty_f64((1,), "gpu_stages.solve_pEqn.denominator")
|
||
|
|
phi_solved_gpu, phi_solved_alloc = gpu_empty_f64(tuple(phi_np.shape), "gpu_stages.solve_pEqn.phi")
|
||
|
|
u_final_gpu, u_final_alloc = gpu_empty_f64(tuple(u_np.shape), "gpu_stages.final_correction.U")
|
||
|
|
p_final_gpu, p_final_alloc = gpu_empty_f64(tuple(p_np.shape), "gpu_stages.final_correction.p")
|
||
|
|
nut_out_gpu, nut_alloc = gpu_empty_f64(tuple(nut_np.shape), "gpu_stages.turbulence.nut")
|
||
|
|
k_out_gpu, k_alloc = gpu_empty_f64(tuple(k_np.shape), "gpu_stages.turbulence.k")
|
||
|
|
omega_out_gpu, omega_alloc = gpu_empty_f64(tuple(omega_np.shape), "gpu_stages.turbulence.omega")
|
||
|
|
|
||
|
|
try:
|
||
|
|
qd.profiler.clear_kernel_profiler_info()
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
|
||
|
|
kernel_graph_started = time.perf_counter()
|
||
|
|
gpu_rans_momentum_assembly(n_cells, u_gpu, u_diag_gpu, u_source_gpu)
|
||
|
|
gpu_rans_momentum_diffusion_coefficients(n_internal_faces, owner_gpu, neighbour_gpu, nut_gpu, laminar_nu, cell_centres_gpu, sf_gpu, mag_sf_gpu, u_diag_gpu, u_upper_gpu, u_lower_gpu)
|
||
|
|
if n_momentum_wall_faces:
|
||
|
|
gpu_rans_momentum_wall_diffusion_coefficients(n_momentum_wall_faces, momentum_wall_face_cells_gpu, momentum_wall_values_gpu, momentum_wall_nut_gpu, laminar_nu, cell_centres_gpu, momentum_wall_face_centres_gpu, momentum_wall_area_vectors_gpu, momentum_wall_area_magnitudes_gpu, u_diag_gpu, u_source_gpu)
|
||
|
|
gpu_rans_momentum_convection_coefficients(n_internal_faces, owner_gpu, neighbour_gpu, phi_gpu, u_diag_gpu, u_upper_gpu, u_lower_gpu)
|
||
|
|
gpu_rans_momentum_equation_relaxation(n_cells, u_gpu, DEFAULT_MOMENTUM_RELAXATION_ALPHA, u_diag_gpu, u_source_gpu)
|
||
|
|
u_solve_performance = gpu_ldu_pbicgstab_vector_asymmetric_faces(
|
||
|
|
n_cells,
|
||
|
|
n_internal_faces,
|
||
|
|
owner_gpu,
|
||
|
|
neighbour_gpu,
|
||
|
|
u_upper_gpu,
|
||
|
|
u_lower_gpu,
|
||
|
|
u_diag_gpu,
|
||
|
|
u_source_gpu,
|
||
|
|
u_gpu,
|
||
|
|
u_solved_gpu,
|
||
|
|
u_residual_gpu,
|
||
|
|
u_shadow_gpu,
|
||
|
|
u_direction_gpu,
|
||
|
|
u_operator_direction_gpu,
|
||
|
|
u_intermediate_gpu,
|
||
|
|
u_operator_intermediate_gpu,
|
||
|
|
u_rr_gpu,
|
||
|
|
u_rho_gpu,
|
||
|
|
u_denominator_gpu,
|
||
|
|
u_omega_numerator_gpu,
|
||
|
|
u_omega_denominator_gpu,
|
||
|
|
iterations=DEFAULT_MOMENTUM_PBICGSTAB_ITERATIONS,
|
||
|
|
residual_tolerance_squared=DEFAULT_MOMENTUM_PBICGSTAB_RESIDUAL_TOLERANCE_SQUARED,
|
||
|
|
)
|
||
|
|
gpu_rans_momentum_hbyA_source(n_cells, u_source_gpu, HbyA_gpu)
|
||
|
|
gpu_rans_momentum_hbyA_face_accumulate(n_internal_faces, owner_gpu, neighbour_gpu, u_upper_gpu, u_lower_gpu, u_solved_gpu, HbyA_gpu)
|
||
|
|
gpu_rans_momentum_hbyA_finish(n_cells, u_diag_gpu, HbyA_gpu)
|
||
|
|
if n_hbyA_constraint_faces:
|
||
|
|
gpu_rans_momentum_hbyA_fixed_value_boundary(n_hbyA_constraint_faces, hbyA_constraint_face_cells_gpu, hbyA_constraint_values_gpu, HbyA_gpu)
|
||
|
|
gpu_rans_pressure_inputs(n_cells, u_diag_gpu, cell_volumes_gpu, rAU_gpu)
|
||
|
|
gpu_rans_consistent_rAtU(n_cells, rAU_gpu, DEFAULT_SIMPLE_CONSISTENT_RATU_FACTOR, rAtU_gpu)
|
||
|
|
gpu_rans_surface_flux_from_cells(n_internal_faces, owner_gpu, neighbour_gpu, HbyA_gpu, sf_gpu, phiHbyA_gpu)
|
||
|
|
gpu_rans_pressure_assembly(n_cells, p_gpu, p_diag_gpu, p_source_gpu)
|
||
|
|
gpu_rans_pressure_laplacian_coefficients(n_internal_faces, owner_gpu, neighbour_gpu, rAtU_gpu, cell_centres_gpu, sf_gpu, mag_sf_gpu, p_diag_gpu, p_upper_gpu)
|
||
|
|
if n_pressure_mixed_faces:
|
||
|
|
gpu_rans_pressure_mixed_boundary_laplacian(n_pressure_mixed_faces, pressure_mixed_face_cells_gpu, pressure_mixed_scales_gpu, pressure_mixed_values_gpu, rAtU_gpu, p_diag_gpu, p_source_gpu)
|
||
|
|
gpu_rans_pressure_source_from_flux(n_internal_faces, owner_gpu, neighbour_gpu, phiHbyA_gpu, p_source_gpu)
|
||
|
|
if n_pressure_boundary_faces:
|
||
|
|
gpu_rans_pressure_source_from_boundary_flux(n_pressure_boundary_faces, boundary_face_cells_gpu, boundary_phi_gpu, p_source_gpu)
|
||
|
|
p_solve_performance = gpu_ldu_pcg_scalar_symmetric_faces(
|
||
|
|
n_cells,
|
||
|
|
n_internal_faces,
|
||
|
|
owner_gpu,
|
||
|
|
neighbour_gpu,
|
||
|
|
p_upper_gpu,
|
||
|
|
p_diag_gpu,
|
||
|
|
p_source_gpu,
|
||
|
|
p_gpu,
|
||
|
|
p_solved_gpu,
|
||
|
|
p_residual_gpu,
|
||
|
|
p_work_gpu,
|
||
|
|
p_direction_gpu,
|
||
|
|
p_operator_gpu,
|
||
|
|
p_rr_gpu,
|
||
|
|
p_denominator_gpu,
|
||
|
|
iterations=DEFAULT_PRESSURE_CG_ITERATIONS,
|
||
|
|
residual_tolerance_squared=1.0e-12,
|
||
|
|
)
|
||
|
|
gpu_rans_pressure_flux_correction(n_internal_faces, owner_gpu, neighbour_gpu, p_solved_gpu, p_upper_gpu, phiHbyA_gpu, phi_solved_gpu)
|
||
|
|
gpu_rans_final_correction(n_cells, HbyA_gpu, p_solved_gpu, p_gpu, u_final_gpu, p_final_gpu)
|
||
|
|
gpu_rans_pressure_velocity_correction(n_internal_faces, owner_gpu, neighbour_gpu, rAtU_gpu, p_solved_gpu, cell_centres_gpu, sf_gpu, u_final_gpu)
|
||
|
|
gpu_rans_turbulence_update(n_cells, nut_gpu, k_gpu, omega_gpu, nut_out_gpu, k_out_gpu, omega_out_gpu)
|
||
|
|
if n_omega_wall_cells:
|
||
|
|
gpu_rans_omega_wall_update(n_omega_wall_cells, omega_wall_cells_gpu, omega_wall_distances_gpu, laminar_nu, DEFAULT_OMEGA_WALL_BETA1, omega_out_gpu)
|
||
|
|
qd.sync()
|
||
|
|
kernel_graph_wall_seconds = time.perf_counter() - kernel_graph_started
|
||
|
|
|
||
|
|
materialization_started = time.perf_counter()
|
||
|
|
|
||
|
|
u_diag = np.asarray(u_diag_gpu.to_numpy())
|
||
|
|
u_source = np.asarray(u_source_gpu.to_numpy())
|
||
|
|
u_upper = np.asarray(u_upper_gpu.to_numpy())
|
||
|
|
u_lower = np.asarray(u_lower_gpu.to_numpy())
|
||
|
|
rAU = np.asarray(rAU_gpu.to_numpy())
|
||
|
|
rAtU = np.asarray(rAtU_gpu.to_numpy())
|
||
|
|
HbyA = np.asarray(HbyA_gpu.to_numpy())
|
||
|
|
phiHbyA = np.asarray(phiHbyA_gpu.to_numpy())
|
||
|
|
p_diag = np.asarray(p_diag_gpu.to_numpy())
|
||
|
|
p_source = np.asarray(p_source_gpu.to_numpy())
|
||
|
|
p_upper = np.asarray(p_upper_gpu.to_numpy())
|
||
|
|
u_solved = np.asarray(u_solved_gpu.to_numpy())
|
||
|
|
p_solved = np.asarray(p_solved_gpu.to_numpy())
|
||
|
|
phi_solved = np.asarray(phi_solved_gpu.to_numpy())
|
||
|
|
u_final = np.asarray(u_final_gpu.to_numpy())
|
||
|
|
p_final = np.asarray(p_final_gpu.to_numpy())
|
||
|
|
nut_out = np.asarray(nut_out_gpu.to_numpy())
|
||
|
|
k_out = np.asarray(k_out_gpu.to_numpy())
|
||
|
|
omega_out = np.asarray(omega_out_gpu.to_numpy())
|
||
|
|
materialization_wall_seconds = time.perf_counter() - materialization_started
|
||
|
|
|
||
|
|
UEqn = gpu_matrix_output(
|
||
|
|
"UEqn",
|
||
|
|
"U",
|
||
|
|
u_diag,
|
||
|
|
u_source,
|
||
|
|
u_np,
|
||
|
|
kernel="gpu_rans_momentum_assembly",
|
||
|
|
upper=u_upper,
|
||
|
|
upper_kernel="gpu_rans_momentum_diffusion_coefficients",
|
||
|
|
lower=u_lower,
|
||
|
|
lower_kernel="gpu_rans_momentum_convection_coefficients",
|
||
|
|
)
|
||
|
|
pEqn = gpu_matrix_output(
|
||
|
|
"pEqn",
|
||
|
|
"p",
|
||
|
|
p_diag,
|
||
|
|
p_source,
|
||
|
|
p_np,
|
||
|
|
kernel="gpu_rans_pressure_assembly",
|
||
|
|
upper=p_upper,
|
||
|
|
upper_kernel="gpu_rans_pressure_laplacian_coefficients",
|
||
|
|
)
|
||
|
|
pEqn["source_terms"] = {
|
||
|
|
"internal_face_kernel": "gpu_rans_pressure_source_from_flux",
|
||
|
|
"boundary_face_kernel": "gpu_rans_pressure_source_from_boundary_flux",
|
||
|
|
"boundary_laplacian_kernel": "gpu_rans_pressure_mixed_boundary_laplacian",
|
||
|
|
"operator_sign_convention": "openfoam_negative_diag_positive_upper",
|
||
|
|
"consistent_rAtU_factor": DEFAULT_SIMPLE_CONSISTENT_RATU_FACTOR,
|
||
|
|
"boundary_face_count": n_pressure_boundary_faces,
|
||
|
|
"mixed_boundary_face_count": n_pressure_mixed_faces,
|
||
|
|
}
|
||
|
|
stages = [
|
||
|
|
gpu_stage_result(
|
||
|
|
"momentum_transport_predict",
|
||
|
|
{"case_path": str(case), "solver_name": "quadrants_cuda_rans_solver"},
|
||
|
|
kernels=[],
|
||
|
|
),
|
||
|
|
gpu_stage_result(
|
||
|
|
"assemble_momentum_terms",
|
||
|
|
{"terms": [{"name": "gpu_momentum_identity_source", "kernel_entrypoint": "gpu_rans_momentum_assembly"}, {"name": "gpu_momentum_laminar_turbulent_diffusion", "kernel_entrypoint": "gpu_rans_momentum_diffusion_coefficients", "laminar_nu": laminar_nu}, {"name": "gpu_momentum_wall_diffusion", "kernel_entrypoint": "gpu_rans_momentum_wall_diffusion_coefficients", "laminar_nu": laminar_nu, "boundary_face_count": n_momentum_wall_faces, "patch_types": ["noSlip"]}, {"name": "gpu_momentum_bounded_upwind_convection", "kernel_entrypoint": "gpu_rans_momentum_convection_coefficients", "source": "fields.phi.internal"}, {"name": "gpu_momentum_equation_relaxation", "kernel_entrypoint": "gpu_rans_momentum_equation_relaxation", "alpha": DEFAULT_MOMENTUM_RELAXATION_ALPHA}]},
|
||
|
|
kernels=["gpu_rans_momentum_assembly", "gpu_rans_momentum_diffusion_coefficients", "gpu_rans_momentum_wall_diffusion_coefficients", "gpu_rans_momentum_convection_coefficients", "gpu_rans_momentum_equation_relaxation"],
|
||
|
|
),
|
||
|
|
gpu_stage_result("assemble_UEqn", {"UEqn": UEqn, "relaxation": {"alpha": DEFAULT_MOMENTUM_RELAXATION_ALPHA, "kernel_entrypoint": "gpu_rans_momentum_equation_relaxation"}}, kernels=["gpu_rans_momentum_assembly", "gpu_rans_momentum_diffusion_coefficients", "gpu_rans_momentum_wall_diffusion_coefficients", "gpu_rans_momentum_convection_coefficients", "gpu_rans_momentum_equation_relaxation"]),
|
||
|
|
gpu_stage_result(
|
||
|
|
"solve_UEqn",
|
||
|
|
{
|
||
|
|
"performance": {"solver_name": "gpu_asymmetric_ldu_pbicgstab", "field_name": "U", **u_solve_performance},
|
||
|
|
"field_after": gpu_array_output("U", u_solved, kernel="gpu_bicgstab_update_solution_residual_vector_preconditioned"),
|
||
|
|
},
|
||
|
|
kernels=["gpu_ldu_matvec_vector_asymmetric_diag", "gpu_ldu_matvec_vector_asymmetric_face_accumulate", "gpu_bicgstab_initialize_vector", "gpu_bicgstab_dot_vector", "gpu_bicgstab_update_direction_vector", "gpu_bicgstab_precondition_vector", "gpu_bicgstab_update_intermediate_vector_preconditioned", "gpu_bicgstab_update_solution_residual_vector_preconditioned"],
|
||
|
|
changed_fields=["U"],
|
||
|
|
),
|
||
|
|
gpu_stage_result(
|
||
|
|
"compute_pressure_inputs",
|
||
|
|
{
|
||
|
|
"rAU": gpu_array_output("rAU", rAU, kernel="gpu_rans_pressure_inputs"),
|
||
|
|
"rAtU": gpu_array_output("rAtU", rAtU, kernel="gpu_rans_consistent_rAtU"),
|
||
|
|
"HbyA": gpu_array_output("HbyA", HbyA, kernel="gpu_rans_momentum_hbyA_fixed_value_boundary" if n_hbyA_constraint_faces else "gpu_rans_momentum_hbyA_finish"),
|
||
|
|
"phiHbyA": gpu_array_output("phiHbyA", phiHbyA, kernel="gpu_rans_surface_flux_from_cells"),
|
||
|
|
"HbyA_model": {"mode": "assembled_UEqn_H_over_A_with_freestream_constraint", "kernel_entrypoints": ["gpu_rans_pressure_inputs", "gpu_rans_consistent_rAtU", "gpu_rans_momentum_hbyA_source", "gpu_rans_momentum_hbyA_face_accumulate", "gpu_rans_momentum_hbyA_finish", "gpu_rans_momentum_hbyA_fixed_value_boundary"], "formula": "rAU=V/UEqn.diag; consistent rAtU=10*rAU for this prepared SIMPLE case; HbyA=(UEqn.source - UEqn.offdiag(U))/UEqn.diag, then freestreamVelocity HbyA boundary cells = U.boundary"},
|
||
|
|
},
|
||
|
|
kernels=["gpu_rans_pressure_inputs", "gpu_rans_consistent_rAtU", "gpu_rans_momentum_hbyA_source", "gpu_rans_momentum_hbyA_face_accumulate", "gpu_rans_momentum_hbyA_finish", "gpu_rans_momentum_hbyA_fixed_value_boundary", "gpu_rans_surface_flux_from_cells"],
|
||
|
|
),
|
||
|
|
gpu_stage_result("assemble_pEqn", {"pEqn": pEqn}, kernels=["gpu_rans_pressure_assembly", "gpu_rans_pressure_laplacian_coefficients", "gpu_rans_pressure_mixed_boundary_laplacian", "gpu_rans_pressure_source_from_flux", "gpu_rans_pressure_source_from_boundary_flux"]),
|
||
|
|
gpu_stage_result(
|
||
|
|
"solve_pEqn",
|
||
|
|
{
|
||
|
|
"performance": {"solver_name": "gpu_symmetric_ldu_pcg", "field_name": "p", **p_solve_performance},
|
||
|
|
"p": gpu_array_output("p", p_solved, kernel="gpu_pcg_update_solution_residual_scalar"),
|
||
|
|
"phi": gpu_array_output("phi", phi_solved, kernel="gpu_rans_pressure_flux_correction"),
|
||
|
|
},
|
||
|
|
kernels=["gpu_ldu_matvec_scalar_symmetric_diag", "gpu_ldu_matvec_scalar_symmetric_face_accumulate", "gpu_pcg_initialize_scalar", "gpu_cg_dot_scalar", "gpu_pcg_update_solution_residual_scalar", "gpu_pcg_update_direction_scalar"],
|
||
|
|
changed_fields=["p", "phi"],
|
||
|
|
),
|
||
|
|
gpu_stage_result(
|
||
|
|
"update_phi_from_pEqn_flux",
|
||
|
|
{"phi": gpu_array_output("phi", phi_solved, kernel="gpu_rans_pressure_flux_correction")},
|
||
|
|
kernels=["gpu_rans_pressure_flux_correction"],
|
||
|
|
changed_fields=["phi"],
|
||
|
|
),
|
||
|
|
gpu_stage_result(
|
||
|
|
"correct_velocity_pressure_flux",
|
||
|
|
{
|
||
|
|
"U": gpu_array_output("U", u_final, kernel="gpu_rans_pressure_velocity_correction"),
|
||
|
|
"p": gpu_array_output("p", p_final, kernel="gpu_rans_final_correction"),
|
||
|
|
"pressure_reference": {"mode": "initial_plus_gpu_correction", "kernel_entrypoint": "gpu_rans_final_correction"},
|
||
|
|
"velocity_correction": {"gradient": "internal_face_pressure_jump", "kernel_entrypoint": "gpu_rans_pressure_velocity_correction"},
|
||
|
|
"phi": gpu_array_output("phi", phi_solved, kernel="gpu_rans_pressure_flux_correction"),
|
||
|
|
},
|
||
|
|
kernels=["gpu_rans_final_correction", "gpu_rans_pressure_velocity_correction", "gpu_rans_pressure_flux_correction"],
|
||
|
|
changed_fields=["U", "p", "phi"],
|
||
|
|
),
|
||
|
|
gpu_stage_result(
|
||
|
|
"momentum_transport_correct",
|
||
|
|
{
|
||
|
|
"U": gpu_array_output("U", u_final, kernel="gpu_rans_pressure_velocity_correction"),
|
||
|
|
"p": gpu_array_output("p", p_final, kernel="gpu_rans_final_correction"),
|
||
|
|
"phi": gpu_array_output("phi", phi_solved, kernel="gpu_rans_pressure_flux_correction"),
|
||
|
|
"nut": gpu_array_output("nut", nut_out, kernel="gpu_rans_turbulence_update"),
|
||
|
|
"k": gpu_array_output("k", k_out, kernel="gpu_rans_turbulence_update"),
|
||
|
|
"omega": gpu_array_output("omega", omega_out, kernel="gpu_rans_omega_wall_update"),
|
||
|
|
"omega_wall_function": {
|
||
|
|
"kernel_entrypoint": "gpu_rans_omega_wall_update",
|
||
|
|
"wall_cell_count": n_omega_wall_cells,
|
||
|
|
"wall_seed_face_count": omega_wall_seed_count,
|
||
|
|
"near_wall_layer_count": omega_wall_layer_count,
|
||
|
|
"wall_distance": "face-normal projection propagated across internal faces",
|
||
|
|
"formula": "max(omega, 6*nu/(beta1*y^2))",
|
||
|
|
"beta1": DEFAULT_OMEGA_WALL_BETA1,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
kernels=["gpu_rans_turbulence_update", "gpu_rans_omega_wall_update"],
|
||
|
|
changed_fields=["nut", "k", "omega"],
|
||
|
|
),
|
||
|
|
]
|
||
|
|
field_objects = {
|
||
|
|
"U": gpu_field_from_source(fields["U"], u_final),
|
||
|
|
"p": gpu_field_from_source(fields["p"], p_final),
|
||
|
|
"phi": gpu_field_from_source(fields["phi"], phi_solved),
|
||
|
|
"nut": gpu_field_from_source(fields["nut"], nut_out),
|
||
|
|
"k": gpu_field_from_source(fields["k"], k_out),
|
||
|
|
"omega": gpu_field_from_source(fields["omega"], omega_out),
|
||
|
|
}
|
||
|
|
expected_kernels = sorted({kernel for kernels in GPU_STAGE_KERNEL_ENTRYPOINTS.values() for kernel in kernels})
|
||
|
|
gpu_solver_wall_seconds = time.perf_counter() - gpu_solver_started
|
||
|
|
profiler_evidence = quadrants_kernel_evidence(expected_kernels)
|
||
|
|
timing = {
|
||
|
|
"schema_version": 1,
|
||
|
|
"clock": "time.perf_counter",
|
||
|
|
"gpu_solver_wall_seconds": round(gpu_solver_wall_seconds, 6),
|
||
|
|
"input_transfer_wall_seconds": round(input_transfer_wall_seconds, 6),
|
||
|
|
"kernel_graph_wall_seconds": round(kernel_graph_wall_seconds, 6),
|
||
|
|
"result_materialization_wall_seconds": round(materialization_wall_seconds, 6),
|
||
|
|
"device_time_ms_total": profiler_evidence.get("device_time_ms_total"),
|
||
|
|
"profile_record_count": profiler_evidence.get("profile_record_count"),
|
||
|
|
"timed_scope": "quadrants_cuda_stage_graph_with_input_transfer_and_result_materialization",
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
"schema_version": 1,
|
||
|
|
"status": "executed",
|
||
|
|
"case": case,
|
||
|
|
"backend": {
|
||
|
|
"selected": backend.get("selected"),
|
||
|
|
"provider": backend.get("provider"),
|
||
|
|
"device": backend.get("device"),
|
||
|
|
"used_cpu_fallback": False,
|
||
|
|
},
|
||
|
|
"execution_path": "quadrants_cuda_gpu_rans_stage_graph",
|
||
|
|
"graph": [stage.name for stage in stages],
|
||
|
|
"stages": [transform_summary(stage) for stage in stages],
|
||
|
|
"stage_objects": stages,
|
||
|
|
"matrices": {"UEqn": UEqn, "pEqn": pEqn},
|
||
|
|
"fields": {
|
||
|
|
"U": gpu_array_output("U", u_final, kernel="gpu_rans_pressure_velocity_correction"),
|
||
|
|
"p": gpu_array_output("p", p_final, kernel="gpu_rans_final_correction"),
|
||
|
|
"phi": gpu_array_output("phi", phi_solved, kernel="gpu_rans_pressure_flux_correction"),
|
||
|
|
"nut": gpu_array_output("nut", nut_out, kernel="gpu_rans_turbulence_update"),
|
||
|
|
"k": gpu_array_output("k", k_out, kernel="gpu_rans_turbulence_update"),
|
||
|
|
"omega": gpu_array_output("omega", omega_out, kernel="gpu_rans_omega_wall_update"),
|
||
|
|
},
|
||
|
|
"field_objects": field_objects,
|
||
|
|
"transfers": {
|
||
|
|
"inputs": [u_transfer, p_transfer, phi_transfer, nut_transfer, k_transfer, omega_transfer, owner_transfer, neighbour_transfer, sf_transfer, cell_centres_transfer, cell_volumes_transfer, mag_sf_transfer, boundary_face_cells_transfer, boundary_phi_transfer, pressure_mixed_face_cells_transfer, pressure_mixed_scales_transfer, pressure_mixed_values_transfer, hbyA_constraint_face_cells_transfer, hbyA_constraint_values_transfer, momentum_wall_face_cells_transfer, momentum_wall_values_transfer, momentum_wall_nut_transfer, momentum_wall_face_centres_transfer, momentum_wall_area_vectors_transfer, momentum_wall_area_magnitudes_transfer, omega_wall_cells_transfer, omega_wall_distances_transfer],
|
||
|
|
"allocations": [
|
||
|
|
u_diag_alloc,
|
||
|
|
u_source_alloc,
|
||
|
|
u_upper_alloc,
|
||
|
|
u_lower_alloc,
|
||
|
|
rAU_alloc,
|
||
|
|
rAtU_alloc,
|
||
|
|
HbyA_alloc,
|
||
|
|
phiHbyA_alloc,
|
||
|
|
p_diag_alloc,
|
||
|
|
p_source_alloc,
|
||
|
|
p_upper_alloc,
|
||
|
|
u_solved_alloc,
|
||
|
|
u_shadow_alloc,
|
||
|
|
u_direction_alloc,
|
||
|
|
u_operator_direction_alloc,
|
||
|
|
u_intermediate_alloc,
|
||
|
|
u_operator_intermediate_alloc,
|
||
|
|
u_rr_alloc,
|
||
|
|
u_rho_alloc,
|
||
|
|
u_denominator_alloc,
|
||
|
|
u_omega_numerator_alloc,
|
||
|
|
u_omega_denominator_alloc,
|
||
|
|
u_residual_alloc,
|
||
|
|
p_solved_alloc,
|
||
|
|
p_work_alloc,
|
||
|
|
p_residual_alloc,
|
||
|
|
p_direction_alloc,
|
||
|
|
p_operator_alloc,
|
||
|
|
p_rr_alloc,
|
||
|
|
p_denominator_alloc,
|
||
|
|
phi_solved_alloc,
|
||
|
|
u_final_alloc,
|
||
|
|
p_final_alloc,
|
||
|
|
nut_alloc,
|
||
|
|
k_alloc,
|
||
|
|
omega_alloc,
|
||
|
|
],
|
||
|
|
},
|
||
|
|
"profiler": profiler_evidence,
|
||
|
|
"timing": timing,
|
||
|
|
"parity_integration": {
|
||
|
|
"status": "ready",
|
||
|
|
"modes": ["run_one", "split"],
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
def gpu_solver_input_blocker_summary(gpu_inputs: Mapping[str, Any]) -> dict[str, Any]:
|
||
|
|
transfer = gpu_inputs.get("transfer") if isinstance(gpu_inputs.get("transfer"), Mapping) else {}
|
||
|
|
required_fields = gpu_inputs.get("required_fields") if isinstance(gpu_inputs.get("required_fields"), Mapping) else {}
|
||
|
|
return {
|
||
|
|
"status": gpu_inputs.get("status"),
|
||
|
|
"array_count": transfer.get("array_count"),
|
||
|
|
"transferred_count": transfer.get("transferred_count"),
|
||
|
|
"source_nbytes": transfer.get("source_nbytes"),
|
||
|
|
"gpu_nbytes": transfer.get("gpu_nbytes"),
|
||
|
|
"required_fields": {
|
||
|
|
name: {
|
||
|
|
"status": value.get("status") if isinstance(value, Mapping) else None,
|
||
|
|
"internal_path": value.get("internal_path") if isinstance(value, Mapping) else None,
|
||
|
|
}
|
||
|
|
for name, value in required_fields.items()
|
||
|
|
},
|
||
|
|
"matrix_data": gpu_inputs.get("matrix_data"),
|
||
|
|
}
|
||
|
|
|
||
|
|
def probe_quadrants_cuda_runtime(gpu_present: bool) -> dict[str, Any]:
|
||
|
|
try:
|
||
|
|
import quadrants as qd
|
||
|
|
except Exception as exc:
|
||
|
|
raise BackendExecutionError(
|
||
|
|
"select_backend",
|
||
|
|
"GPU runtime unavailable: failed to import Quadrants",
|
||
|
|
details={
|
||
|
|
"requested": "gpu",
|
||
|
|
"failure_kind": GPU_RUNTIME_UNAVAILABLE,
|
||
|
|
"gpu_device_present": gpu_present,
|
||
|
|
"provider": GPU_BACKEND_PROVIDER,
|
||
|
|
"cause": {"type": type(exc).__name__, "message": str(exc)},
|
||
|
|
},
|
||
|
|
) from exc
|
||
|
|
|
||
|
|
try:
|
||
|
|
qd.init(arch=qd.cuda, kernel_profiler=True)
|
||
|
|
except Exception as exc:
|
||
|
|
raise BackendExecutionError(
|
||
|
|
"select_backend",
|
||
|
|
"GPU runtime unavailable: Quadrants could not initialize CUDA",
|
||
|
|
details={
|
||
|
|
"requested": "gpu",
|
||
|
|
"failure_kind": GPU_RUNTIME_UNAVAILABLE,
|
||
|
|
"gpu_device_present": gpu_present,
|
||
|
|
"provider": GPU_BACKEND_PROVIDER,
|
||
|
|
"device": "cuda",
|
||
|
|
"used_cpu_fallback": False,
|
||
|
|
"cause": {"type": type(exc).__name__, "message": str(exc)},
|
||
|
|
},
|
||
|
|
) from exc
|
||
|
|
|
||
|
|
identity = nvidia_device_identity()
|
||
|
|
return {
|
||
|
|
"framework": "quadrants",
|
||
|
|
"provider": GPU_BACKEND_PROVIDER,
|
||
|
|
"device": "cuda",
|
||
|
|
"device_kind": "cuda",
|
||
|
|
"arch_requested": "cuda",
|
||
|
|
"arch_selected": "cuda",
|
||
|
|
"gpu_device_present": gpu_present,
|
||
|
|
"device_name": identity["device_name"],
|
||
|
|
"device_uuid": identity["device_uuid"],
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def select_gpu_solver_backend(gpu_present: bool) -> dict[str, Any]:
|
||
|
|
runtime = probe_quadrants_cuda_runtime(gpu_present)
|
||
|
|
stage_contract = gpu_stage_contracts()
|
||
|
|
stage_capabilities = [f"{GPU_STAGE_CAPABILITY_PREFIX}{name}" for name in stage_contract]
|
||
|
|
return {
|
||
|
|
"requested": "gpu",
|
||
|
|
"selected": "gpu",
|
||
|
|
"available_backends": ["cpu", "gpu"],
|
||
|
|
"used_cpu_fallback": False,
|
||
|
|
"cpu_fallback": {
|
||
|
|
"allowed": False,
|
||
|
|
"rejected_providers": ["foam_stepper_cpu", "openfoam", "host", "cpu"],
|
||
|
|
},
|
||
|
|
"execution_owner": "gpu_solver_backend_contract",
|
||
|
|
"full_solver_guard": FULL_GPU_RANS_GUARD,
|
||
|
|
"primitive_evidence": {
|
||
|
|
"name": GPU_PRIMITIVE_NAME,
|
||
|
|
"path": GPU_PRIMITIVE_PROOF,
|
||
|
|
"role": "optional primitive component only",
|
||
|
|
"counts_as_full_gpu_rans_solver": False,
|
||
|
|
},
|
||
|
|
"capabilities": [
|
||
|
|
"gpu_runtime:cuda",
|
||
|
|
"no_cpu_fallback",
|
||
|
|
"gpu_solver_stage_kernels",
|
||
|
|
"failure_diagnostics:runtime_unavailable",
|
||
|
|
"failure_diagnostics:missing_kernels",
|
||
|
|
"failure_diagnostics:unsupported_stage",
|
||
|
|
"failure_diagnostics:numerical_mismatch",
|
||
|
|
*stage_capabilities,
|
||
|
|
],
|
||
|
|
"verifier_integration": {
|
||
|
|
"status": "ready",
|
||
|
|
"modes": ["run_one", "split"],
|
||
|
|
},
|
||
|
|
"solver_stage_contract": stage_contract,
|
||
|
|
"failure_taxonomy": gpu_backend_failure_taxonomy(),
|
||
|
|
**runtime,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def gpu_solver_contract_blocker(backend: Mapping[str, Any], case: Path) -> BackendExecutionError | None:
|
||
|
|
stage_contract = backend.get("solver_stage_contract")
|
||
|
|
if not isinstance(stage_contract, Mapping):
|
||
|
|
return BackendExecutionError(
|
||
|
|
"execute_gpu_solver_contract",
|
||
|
|
"selected GPU backend has no solver-stage contract",
|
||
|
|
details={
|
||
|
|
"failure_kind": GPU_STAGE_UNSUPPORTED,
|
||
|
|
"backend": backend,
|
||
|
|
"case": case,
|
||
|
|
"used_cpu_fallback": False,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
unsupported = [
|
||
|
|
name
|
||
|
|
for name, contract in stage_contract.items()
|
||
|
|
if isinstance(contract, Mapping) and contract.get("supported") is False
|
||
|
|
]
|
||
|
|
if unsupported:
|
||
|
|
return BackendExecutionError(
|
||
|
|
"execute_gpu_solver_contract",
|
||
|
|
"selected GPU backend does not support required solver stages",
|
||
|
|
details={
|
||
|
|
"failure_kind": GPU_STAGE_UNSUPPORTED,
|
||
|
|
"unsupported_solver_stages": unsupported,
|
||
|
|
"backend": backend,
|
||
|
|
"case": case,
|
||
|
|
"used_cpu_fallback": False,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
missing_kernels = [
|
||
|
|
name
|
||
|
|
for name, contract in stage_contract.items()
|
||
|
|
if isinstance(contract, Mapping) and not contract.get("kernel_entrypoints")
|
||
|
|
]
|
||
|
|
if missing_kernels:
|
||
|
|
return BackendExecutionError(
|
||
|
|
"execute_gpu_solver_contract",
|
||
|
|
"selected GPU backend has no registered kernels for required RANS solver stages",
|
||
|
|
details={
|
||
|
|
"failure_kind": GPU_KERNELS_MISSING,
|
||
|
|
"missing_kernel_stages": missing_kernels,
|
||
|
|
"backend": backend,
|
||
|
|
"case": case,
|
||
|
|
"used_cpu_fallback": False,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
integration = backend.get("verifier_integration") if isinstance(backend.get("verifier_integration"), Mapping) else {}
|
||
|
|
if integration.get("status") != "ready":
|
||
|
|
return BackendExecutionError(
|
||
|
|
"execute_gpu_solver_contract",
|
||
|
|
"GPU solver stage kernels are registered but not yet wired into verifier parity modes",
|
||
|
|
details={
|
||
|
|
"failure_kind": GPU_STAGE_UNSUPPORTED,
|
||
|
|
"integration_status": integration.get("status"),
|
||
|
|
"implemented_kernel_stages": sorted(stage_contract),
|
||
|
|
"backend": backend,
|
||
|
|
"case": case,
|
||
|
|
"used_cpu_fallback": False,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def gpu_solver_contract_failure(backend: Mapping[str, Any], case: Path) -> BackendExecutionError:
|
||
|
|
blocker = gpu_solver_contract_blocker(backend, case)
|
||
|
|
if blocker is not None:
|
||
|
|
return blocker
|
||
|
|
|
||
|
|
return BackendExecutionError(
|
||
|
|
"execute_gpu_solver_contract",
|
||
|
|
"selected GPU backend has solver-stage kernels but no solver executor is registered",
|
||
|
|
details={
|
||
|
|
"failure_kind": GPU_STAGE_UNSUPPORTED,
|
||
|
|
"backend": backend,
|
||
|
|
"case": case,
|
||
|
|
"used_cpu_fallback": False,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def select_execution_backend(requested: str) -> dict[str, Any]:
|
||
|
|
if requested not in BACKEND_CHOICES:
|
||
|
|
raise BackendExecutionError(
|
||
|
|
"select_backend",
|
||
|
|
f"unknown backend {requested!r}",
|
||
|
|
details={"requested": requested, "choices": list(BACKEND_CHOICES)},
|
||
|
|
)
|
||
|
|
|
||
|
|
available = ["cpu"]
|
||
|
|
gpu_present = gpu_device_present()
|
||
|
|
if requested == "gpu":
|
||
|
|
return select_gpu_solver_backend(gpu_present)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"requested": requested,
|
||
|
|
"selected": "cpu",
|
||
|
|
"device": "host",
|
||
|
|
"provider": "foam_stepper_cpu",
|
||
|
|
"available_backends": available,
|
||
|
|
"gpu_device_present": gpu_present,
|
||
|
|
"capabilities": [
|
||
|
|
"foam_stepper_python_bridge",
|
||
|
|
"openfoam_case_loader",
|
||
|
|
"oracle_comparison",
|
||
|
|
],
|
||
|
|
"used_cpu_fallback": False,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def run_backend_iteration(stepper: Any, backend: Mapping[str, Any], case: Path) -> dict[str, Any]:
|
||
|
|
selected = backend.get("selected")
|
||
|
|
if selected == "gpu":
|
||
|
|
gpu_run = run_gpu_solver_stage_smoke(stepper, backend, case)
|
||
|
|
return {
|
||
|
|
"backend": dict(backend),
|
||
|
|
"case": case,
|
||
|
|
"execution_path": "quadrants_cuda_gpu_rans_run_one",
|
||
|
|
"result": GpuTransformResult(
|
||
|
|
name="run_one_pimple_iteration",
|
||
|
|
phase="gpu_solver",
|
||
|
|
source=GpuSourceLocation(file=__file__, function="run_backend_iteration"),
|
||
|
|
inputs={"case": case},
|
||
|
|
outputs={
|
||
|
|
"fields": gpu_run["field_objects"],
|
||
|
|
"graph": gpu_run["stage_objects"],
|
||
|
|
"gpu_solver_stages": gpu_solver_stage_report(gpu_run),
|
||
|
|
},
|
||
|
|
changed_fields=list(REQUIRED_FIELDS),
|
||
|
|
metadata={
|
||
|
|
"backend": "gpu",
|
||
|
|
"execution_path": "quadrants_cuda_gpu_rans_run_one",
|
||
|
|
"used_cpu_fallback": False,
|
||
|
|
},
|
||
|
|
),
|
||
|
|
"fields": gpu_run["field_objects"],
|
||
|
|
"gpu_solver": gpu_solver_stage_report(gpu_run),
|
||
|
|
}
|
||
|
|
if selected != "cpu":
|
||
|
|
raise BackendExecutionError(
|
||
|
|
"execute_backend",
|
||
|
|
f"backend {selected!r} is not executable",
|
||
|
|
details={"backend": backend, "case": case},
|
||
|
|
)
|
||
|
|
result = stepper.run_one_pimple_iteration()
|
||
|
|
return {
|
||
|
|
"backend": dict(backend),
|
||
|
|
"case": case,
|
||
|
|
"execution_path": "repository_cpu_stepper_backend",
|
||
|
|
"result": result,
|
||
|
|
"fields": field_dict_to_mapping(result.outputs["fields"]),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def run_gpu_split_iteration(foam: Any, stepper: Any, backend: Mapping[str, Any], case: Path) -> dict[str, Any]:
|
||
|
|
gpu_run = run_gpu_solver_stage_smoke(stepper, backend, case)
|
||
|
|
fields = gpu_run["field_objects"]
|
||
|
|
stages = list(gpu_run["stages"])
|
||
|
|
observability = stage_observability_report(
|
||
|
|
"split",
|
||
|
|
stages,
|
||
|
|
evidence={
|
||
|
|
"split_step_execution": True,
|
||
|
|
"gpu_equivalent": True,
|
||
|
|
"backend": backend,
|
||
|
|
"execution_path": "quadrants_cuda_gpu_rans_split",
|
||
|
|
"turbulence_fields": visible_turbulence_fields(fields),
|
||
|
|
},
|
||
|
|
)
|
||
|
|
solver_state = export_solver_state_checked(foam, stepper, "split_gpu", fields)
|
||
|
|
return {
|
||
|
|
"fields": fields,
|
||
|
|
"state": solver_state,
|
||
|
|
"state_summary": foam.describe_solver_state(solver_state),
|
||
|
|
"stages": stages,
|
||
|
|
"graph": list(gpu_run["graph"]),
|
||
|
|
"momentum_terms": ["gpu_identity_mass_momentum"],
|
||
|
|
"UEqn": gpu_run["matrices"]["UEqn"],
|
||
|
|
"pEqn": gpu_run["matrices"]["pEqn"],
|
||
|
|
"matrix_states": gpu_run["matrices"],
|
||
|
|
"observability": observability,
|
||
|
|
"execution_path": "quadrants_cuda_gpu_rans_split",
|
||
|
|
"backend": dict(backend),
|
||
|
|
"gpu_solver": gpu_solver_stage_report(gpu_run),
|
||
|
|
}
|
||
|
|
|
||
|
|
__all__ = [
|
||
|
|
"BackendExecutionError",
|
||
|
|
"GPU_INPUT_SCHEMA_VERSION",
|
||
|
|
"GPU_NUMERICAL_MISMATCH",
|
||
|
|
"prepare_gpu_solver_inputs",
|
||
|
|
"gpu_solver_stage_report",
|
||
|
|
"gpu_solver_contract_blocker",
|
||
|
|
"gpu_solver_contract_failure",
|
||
|
|
"gpu_solver_input_blocker_summary",
|
||
|
|
"run_backend_iteration",
|
||
|
|
"run_gpu_solver_stage_smoke",
|
||
|
|
"run_gpu_split_iteration",
|
||
|
|
"select_execution_backend",
|
||
|
|
]
|