feat(linux): portable build works on linux + benchmarks
This commit is contained in:
373
benchmarks/backend_operator.cpp
Normal file
373
benchmarks/backend_operator.cpp
Normal file
@@ -0,0 +1,373 @@
|
||||
#include <meson_mfem_template/config.hpp>
|
||||
#include <mfem.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#if !MESON_MFEM_HAS_MPI || !MESON_MFEM_HAS_HYPRE
|
||||
#error "The backend benchmark requires MFEM with MPI and Hypre"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr const char *json_prefix = "MFEM_BENCHMARK_JSON ";
|
||||
|
||||
double AnalyticField(const mfem::Vector &point)
|
||||
{
|
||||
constexpr double pi = 3.141592653589793238462643383279502884;
|
||||
return std::sin(pi * point[0]) * std::sin(pi * point[1]) *
|
||||
std::sin(pi * point[2]);
|
||||
}
|
||||
|
||||
void SynchronizeDevice(bool uses_gpu)
|
||||
{
|
||||
if (uses_gpu) { MFEM_DEVICE_SYNC; }
|
||||
}
|
||||
|
||||
double MaxTimedRegion(MPI_Comm communicator, bool uses_gpu,
|
||||
const std::function<void()> &operation)
|
||||
{
|
||||
SynchronizeDevice(uses_gpu);
|
||||
MPI_Barrier(communicator);
|
||||
const double begin = MPI_Wtime();
|
||||
operation();
|
||||
SynchronizeDevice(uses_gpu);
|
||||
const double local_seconds = MPI_Wtime() - begin;
|
||||
double maximum_seconds = 0.0;
|
||||
MPI_Allreduce(&local_seconds, &maximum_seconds, 1, MPI_DOUBLE, MPI_MAX,
|
||||
communicator);
|
||||
return maximum_seconds;
|
||||
}
|
||||
|
||||
std::string JsonEscape(const std::string &value)
|
||||
{
|
||||
std::ostringstream escaped;
|
||||
for (const char character : value)
|
||||
{
|
||||
switch (character)
|
||||
{
|
||||
case '\\': escaped << "\\\\"; break;
|
||||
case '"': escaped << "\\\""; break;
|
||||
case '\n': escaped << "\\n"; break;
|
||||
case '\r': escaped << "\\r"; break;
|
||||
case '\t': escaped << "\\t"; break;
|
||||
default: escaped << character; break;
|
||||
}
|
||||
}
|
||||
return escaped.str();
|
||||
}
|
||||
|
||||
int PositiveEnvironmentInteger(const char *name, int fallback)
|
||||
{
|
||||
const char *value = std::getenv(name);
|
||||
if (value == nullptr) { return fallback; }
|
||||
char *end = nullptr;
|
||||
const long parsed = std::strtol(value, &end, 10);
|
||||
return end != value && *end == '\0' && parsed > 0
|
||||
? static_cast<int>(parsed)
|
||||
: fallback;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
mfem::Mpi::Init(argc, argv);
|
||||
mfem::Hypre::Init();
|
||||
MPI_Comm communicator = MPI_COMM_WORLD;
|
||||
const int rank = mfem::Mpi::WorldRank();
|
||||
const int ranks = mfem::Mpi::WorldSize();
|
||||
const double process_begin = MPI_Wtime();
|
||||
|
||||
const char *device_name = "cpu";
|
||||
int mesh_n = 32;
|
||||
int order = 3;
|
||||
int applications = 50;
|
||||
int warmup_applications = 5;
|
||||
int maximum_applications = 1000000;
|
||||
int maximum_iterations = 1000;
|
||||
int trial = 0;
|
||||
double relative_tolerance = 1.0e-6;
|
||||
double minimum_apply_seconds = 0.0;
|
||||
bool run_solve = false;
|
||||
|
||||
mfem::OptionsParser options(argc, argv);
|
||||
options.AddOption(&device_name, "-d", "--device",
|
||||
"MFEM device string (cpu, omp, ceed-cpu, cuda, ...). ");
|
||||
options.AddOption(&mesh_n, "-n", "--mesh-n",
|
||||
"Elements per dimension in the fixed global mesh.");
|
||||
options.AddOption(&order, "-o", "--order", "H1 polynomial order.");
|
||||
options.AddOption(&applications, "-a", "--applications",
|
||||
"Number of timed distributed operator applications.");
|
||||
options.AddOption(&warmup_applications, "-w", "--warmup-applications",
|
||||
"Untimed operator applications before measurement.");
|
||||
options.AddOption(&maximum_applications, "-ma", "--max-applications",
|
||||
"Safety cap for automatically calibrated applications.");
|
||||
options.AddOption(&minimum_apply_seconds, "-mt", "--minimum-apply-seconds",
|
||||
"Minimum final timing window; zero keeps the requested count.");
|
||||
options.AddOption(&maximum_iterations, "-m", "--max-iterations",
|
||||
"Maximum iterations for the identically configured CG solve.");
|
||||
options.AddOption(&relative_tolerance, "-r", "--relative-tolerance",
|
||||
"Relative tolerance for the identically configured CG solve.");
|
||||
options.AddOption(&run_solve, "-s", "--solve", "-no-s", "--no-solve",
|
||||
"Run the optional Jacobi-preconditioned CG validation solve.");
|
||||
options.AddOption(&trial, "-t", "--trial",
|
||||
"Trial identifier copied into the JSON record.");
|
||||
options.Parse();
|
||||
if (!options.Good())
|
||||
{
|
||||
if (rank == 0) { options.PrintUsage(std::cerr); }
|
||||
return 2;
|
||||
}
|
||||
if (mesh_n < 2 || order < 1 || applications < 1 ||
|
||||
warmup_applications < 0 || maximum_applications < applications ||
|
||||
maximum_iterations < 1 || relative_tolerance <= 0.0 ||
|
||||
minimum_apply_seconds < 0.0)
|
||||
{
|
||||
if (rank == 0)
|
||||
{
|
||||
std::cerr << "All sizes/counts must be positive (warmups may be zero).\n";
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
|
||||
std::unique_ptr<mfem::Device> device;
|
||||
MPI_Barrier(communicator);
|
||||
const double device_begin = MPI_Wtime();
|
||||
device = std::make_unique<mfem::Device>(device_name);
|
||||
const bool uses_gpu = mfem::Device::Allows(
|
||||
mfem::Backend::CUDA_MASK | mfem::Backend::HIP_MASK);
|
||||
SynchronizeDevice(uses_gpu);
|
||||
const double local_device_seconds = MPI_Wtime() - device_begin;
|
||||
double device_seconds = 0.0;
|
||||
MPI_Allreduce(&local_device_seconds, &device_seconds, 1, MPI_DOUBLE, MPI_MAX,
|
||||
communicator);
|
||||
|
||||
std::unique_ptr<mfem::ParMesh> mesh;
|
||||
const double mesh_seconds = MaxTimedRegion(communicator, uses_gpu, [&]() {
|
||||
mfem::Mesh serial_mesh = mfem::Mesh::MakeCartesian3D(
|
||||
mesh_n, mesh_n, mesh_n, mfem::Element::HEXAHEDRON);
|
||||
mfem::Array<int> partitioning(serial_mesh.GetNE());
|
||||
for (int element = 0; element < serial_mesh.GetNE(); ++element)
|
||||
{
|
||||
partitioning[element] = static_cast<long long>(element) * ranks /
|
||||
serial_mesh.GetNE();
|
||||
}
|
||||
mesh = std::make_unique<mfem::ParMesh>(communicator, serial_mesh,
|
||||
partitioning.GetData());
|
||||
});
|
||||
|
||||
std::unique_ptr<mfem::H1_FECollection> elements;
|
||||
std::unique_ptr<mfem::ParFiniteElementSpace> space;
|
||||
const double space_seconds = MaxTimedRegion(communicator, uses_gpu, [&]() {
|
||||
elements = std::make_unique<mfem::H1_FECollection>(
|
||||
order, mesh->Dimension(), mfem::BasisType::GaussLobatto);
|
||||
space = std::make_unique<mfem::ParFiniteElementSpace>(mesh.get(),
|
||||
elements.get());
|
||||
});
|
||||
|
||||
mfem::Array<int> essential_boundary(mesh->bdr_attributes.Max());
|
||||
essential_boundary = 1;
|
||||
mfem::Array<int> essential_dofs;
|
||||
space->GetEssentialTrueDofs(essential_boundary, essential_dofs);
|
||||
|
||||
mfem::ConstantCoefficient one(1.0);
|
||||
auto right_hand_side = std::make_unique<mfem::ParLinearForm>(space.get());
|
||||
auto solution = std::make_unique<mfem::ParGridFunction>(space.get());
|
||||
auto diffusion = std::make_unique<mfem::ParBilinearForm>(space.get());
|
||||
const double assembly_seconds = MaxTimedRegion(communicator, uses_gpu, [&]() {
|
||||
right_hand_side->AddDomainIntegrator(new mfem::DomainLFIntegrator(one));
|
||||
right_hand_side->Assemble();
|
||||
*solution = 0.0;
|
||||
diffusion->SetAssemblyLevel(mfem::AssemblyLevel::PARTIAL);
|
||||
diffusion->AddDomainIntegrator(new mfem::DiffusionIntegrator(one));
|
||||
diffusion->Assemble();
|
||||
});
|
||||
|
||||
mfem::OperatorPtr system_operator;
|
||||
mfem::Vector linear_right_hand_side;
|
||||
mfem::Vector linear_solution;
|
||||
const double form_seconds = MaxTimedRegion(communicator, uses_gpu, [&]() {
|
||||
diffusion->FormLinearSystem(essential_dofs, *solution, *right_hand_side,
|
||||
system_operator, linear_solution,
|
||||
linear_right_hand_side);
|
||||
});
|
||||
|
||||
std::unique_ptr<mfem::OperatorJacobiSmoother> preconditioner;
|
||||
double preconditioner_seconds = 0.0;
|
||||
if (run_solve)
|
||||
{
|
||||
preconditioner_seconds = MaxTimedRegion(
|
||||
communicator, uses_gpu, [&]() {
|
||||
preconditioner = std::make_unique<mfem::OperatorJacobiSmoother>(
|
||||
*diffusion, essential_dofs);
|
||||
});
|
||||
}
|
||||
|
||||
mfem::FunctionCoefficient analytic(AnalyticField);
|
||||
mfem::ParGridFunction probe_field(space.get());
|
||||
probe_field.ProjectCoefficient(analytic);
|
||||
mfem::Vector probe_input;
|
||||
probe_field.GetTrueDofs(probe_input);
|
||||
mfem::Vector probe_output(system_operator->Height());
|
||||
probe_input.UseDevice(true);
|
||||
probe_output.UseDevice(true);
|
||||
|
||||
for (int application = 0; application < warmup_applications; ++application)
|
||||
{
|
||||
system_operator->Mult(probe_input, probe_output);
|
||||
}
|
||||
SynchronizeDevice(uses_gpu);
|
||||
|
||||
const int requested_applications = applications;
|
||||
int timed_applications = requested_applications;
|
||||
if (minimum_apply_seconds > 0.0)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
const double calibration_seconds = MaxTimedRegion(
|
||||
communicator, uses_gpu, [&]() {
|
||||
for (int application = 0; application < timed_applications;
|
||||
++application)
|
||||
{
|
||||
system_operator->Mult(probe_input, probe_output);
|
||||
}
|
||||
});
|
||||
if (calibration_seconds >= minimum_apply_seconds ||
|
||||
timed_applications == maximum_applications)
|
||||
{
|
||||
break;
|
||||
}
|
||||
timed_applications = timed_applications > maximum_applications / 2
|
||||
? maximum_applications
|
||||
: timed_applications * 2;
|
||||
}
|
||||
}
|
||||
const double apply_seconds = MaxTimedRegion(
|
||||
communicator, uses_gpu, [&]() {
|
||||
for (int application = 0; application < timed_applications;
|
||||
++application)
|
||||
{
|
||||
system_operator->Mult(probe_input, probe_output);
|
||||
}
|
||||
});
|
||||
|
||||
const double probe_norm = std::sqrt(
|
||||
mfem::InnerProduct(communicator, probe_output, probe_output));
|
||||
double solve_seconds = 0.0;
|
||||
int cg_iterations = 0;
|
||||
bool cg_converged = false;
|
||||
double cg_final_norm = 0.0;
|
||||
double relative_residual = 0.0;
|
||||
double solution_norm = 0.0;
|
||||
if (run_solve)
|
||||
{
|
||||
mfem::CGSolver solver(communicator);
|
||||
solver.SetPreconditioner(*preconditioner);
|
||||
solver.SetOperator(*system_operator);
|
||||
solver.SetRelTol(relative_tolerance);
|
||||
solver.SetAbsTol(0.0);
|
||||
solver.SetPrintLevel(-1);
|
||||
solver.iterative_mode = false;
|
||||
|
||||
linear_solution = 0.0;
|
||||
solver.SetMaxIter(2);
|
||||
solver.Mult(linear_right_hand_side, linear_solution);
|
||||
SynchronizeDevice(uses_gpu);
|
||||
|
||||
linear_solution = 0.0;
|
||||
solver.SetMaxIter(maximum_iterations);
|
||||
solve_seconds = MaxTimedRegion(communicator, uses_gpu, [&]() {
|
||||
solver.Mult(linear_right_hand_side, linear_solution);
|
||||
});
|
||||
cg_iterations = solver.GetNumIterations();
|
||||
cg_converged = solver.GetConverged();
|
||||
cg_final_norm = solver.GetFinalNorm();
|
||||
|
||||
mfem::Vector applied_solution(system_operator->Height());
|
||||
mfem::Vector residual(linear_right_hand_side);
|
||||
system_operator->Mult(linear_solution, applied_solution);
|
||||
residual -= applied_solution;
|
||||
SynchronizeDevice(uses_gpu);
|
||||
|
||||
const double right_hand_side_norm_squared = mfem::InnerProduct(
|
||||
communicator, linear_right_hand_side, linear_right_hand_side);
|
||||
const double residual_norm_squared = mfem::InnerProduct(
|
||||
communicator, residual, residual);
|
||||
relative_residual = right_hand_side_norm_squared > 0.0
|
||||
? std::sqrt(residual_norm_squared / right_hand_side_norm_squared)
|
||||
: std::sqrt(residual_norm_squared);
|
||||
solution_norm = std::sqrt(
|
||||
mfem::InnerProduct(communicator, linear_solution, linear_solution));
|
||||
}
|
||||
|
||||
const auto global_dofs = space->GlobalTrueVSize();
|
||||
const auto global_elements = mesh->GetGlobalNE();
|
||||
const double mdof_per_second =
|
||||
1.0e-6 * static_cast<double>(global_dofs) * timed_applications /
|
||||
apply_seconds;
|
||||
const int omp_threads = PositiveEnvironmentInteger("OMP_NUM_THREADS", 1);
|
||||
const bool solve_valid = !run_solve ||
|
||||
(cg_converged && std::isfinite(relative_residual) &&
|
||||
relative_residual <= 10.0 * relative_tolerance &&
|
||||
std::isfinite(solution_norm) && solution_norm > 0.0);
|
||||
const bool valid = solve_valid && std::isfinite(probe_norm) &&
|
||||
probe_norm > 0.0;
|
||||
|
||||
SynchronizeDevice(uses_gpu);
|
||||
MPI_Barrier(communicator);
|
||||
const double local_total_seconds = MPI_Wtime() - process_begin;
|
||||
double total_seconds = 0.0;
|
||||
MPI_Allreduce(&local_total_seconds, &total_seconds, 1, MPI_DOUBLE, MPI_MAX,
|
||||
communicator);
|
||||
|
||||
if (rank == 0)
|
||||
{
|
||||
std::cout << std::setprecision(17) << json_prefix
|
||||
<< '{'
|
||||
<< "\"schema_version\":1,"
|
||||
<< "\"trial\":" << trial << ','
|
||||
<< "\"device\":\"" << JsonEscape(device_name) << "\","
|
||||
<< "\"ranks\":" << ranks << ','
|
||||
<< "\"omp_threads\":" << omp_threads << ','
|
||||
<< "\"dimension\":3,"
|
||||
<< "\"mesh_n\":" << mesh_n << ','
|
||||
<< "\"order\":" << order << ','
|
||||
<< "\"global_elements\":" << global_elements << ','
|
||||
<< "\"global_true_dofs\":" << global_dofs << ','
|
||||
<< "\"assembly\":\"partial\","
|
||||
<< "\"warmup_applications\":" << warmup_applications << ','
|
||||
<< "\"requested_applications\":" << requested_applications << ','
|
||||
<< "\"applications\":" << timed_applications << ','
|
||||
<< "\"minimum_apply_seconds\":" << minimum_apply_seconds << ','
|
||||
<< "\"device_seconds\":" << device_seconds << ','
|
||||
<< "\"mesh_seconds\":" << mesh_seconds << ','
|
||||
<< "\"space_seconds\":" << space_seconds << ','
|
||||
<< "\"assembly_seconds\":" << assembly_seconds << ','
|
||||
<< "\"form_seconds\":" << form_seconds << ','
|
||||
<< "\"preconditioner_seconds\":" << preconditioner_seconds << ','
|
||||
<< "\"apply_seconds\":" << apply_seconds << ','
|
||||
<< "\"apply_mdof_per_second\":" << mdof_per_second << ','
|
||||
<< "\"solve_ran\":" << (run_solve ? "true" : "false") << ','
|
||||
<< "\"solve_seconds\":" << solve_seconds << ','
|
||||
<< "\"cg_iterations\":" << cg_iterations << ','
|
||||
<< "\"cg_converged\":"
|
||||
<< (cg_converged ? "true" : "false") << ','
|
||||
<< "\"cg_final_norm\":" << cg_final_norm << ','
|
||||
<< "\"verified_relative_residual\":" << relative_residual << ','
|
||||
<< "\"probe_norm\":" << probe_norm << ','
|
||||
<< "\"solution_norm\":" << solution_norm << ','
|
||||
<< "\"total_seconds\":" << total_seconds << ','
|
||||
<< "\"valid\":" << (valid ? "true" : "false")
|
||||
<< "}\n";
|
||||
}
|
||||
|
||||
return valid ? 0 : 3;
|
||||
}
|
||||
142
benchmarks/meson.build
Normal file
142
benchmarks/meson.build
Normal file
@@ -0,0 +1,142 @@
|
||||
benchmark_executable = []
|
||||
benchmark_mpi_launcher = ''
|
||||
|
||||
if get_option('build_benchmarks')
|
||||
if is_wasm
|
||||
error('build_benchmarks requires a native target; use the web demo for Emscripten')
|
||||
endif
|
||||
if not mfem_has_mpi
|
||||
error('build_benchmarks requires -Dmfem_mpi=enabled (and therefore Hypre)')
|
||||
endif
|
||||
|
||||
benchmark_accelerator_deps = []
|
||||
if mfem_has_cuda
|
||||
benchmark_nvcc = dependency_has_nvcc ? (
|
||||
dependency_prefix / 'bin' / 'nvcc'
|
||||
) : nvcc_program.full_path()
|
||||
benchmark_cuda_root_result = run_command(
|
||||
python_build,
|
||||
'-c',
|
||||
'import os, sys; print(os.path.dirname(os.path.dirname(os.path.realpath(sys.argv[1]))))',
|
||||
benchmark_nvcc,
|
||||
check: true,
|
||||
)
|
||||
benchmark_cuda_root = benchmark_cuda_root_result.stdout().strip()
|
||||
benchmark_accelerator_deps += [cpp.find_library(
|
||||
'cudart',
|
||||
dirs: [
|
||||
benchmark_cuda_root / 'lib64',
|
||||
benchmark_cuda_root / 'lib',
|
||||
benchmark_cuda_root / 'lib' / 'x64',
|
||||
benchmark_cuda_root / 'targets' / 'x86_64-linux' / 'lib',
|
||||
benchmark_cuda_root / 'targets' / 'aarch64-linux' / 'lib',
|
||||
benchmark_cuda_root / 'targets' / 'ppc64le-linux' / 'lib',
|
||||
],
|
||||
required: true,
|
||||
)]
|
||||
endif
|
||||
if mfem_has_hip
|
||||
benchmark_hipcc = dependency_has_hipcc ? (
|
||||
dependency_prefix / 'bin' / 'hipcc'
|
||||
) : hipcc_program.full_path()
|
||||
benchmark_hip_root_result = run_command(
|
||||
python_build,
|
||||
'-c',
|
||||
'import os, sys; print(os.path.dirname(os.path.dirname(os.path.realpath(sys.argv[1]))))',
|
||||
benchmark_hipcc,
|
||||
check: true,
|
||||
)
|
||||
benchmark_hip_root = benchmark_hip_root_result.stdout().strip()
|
||||
benchmark_accelerator_deps += [cpp.find_library(
|
||||
'amdhip64',
|
||||
dirs: [benchmark_hip_root / 'lib', benchmark_hip_root / 'lib64'],
|
||||
required: true,
|
||||
)]
|
||||
endif
|
||||
|
||||
benchmark_build_rpath = mfem_runtime_prefix == '' ? '' : mfem_runtime_prefix / 'lib'
|
||||
benchmark_install_rpath = ''
|
||||
if mfem_runtime_prefix != '' and not is_windows
|
||||
benchmark_install_rpath = host_machine.system() == 'darwin' ? '@loader_path/../lib' : '$ORIGIN/../lib'
|
||||
endif
|
||||
|
||||
benchmark_executable = executable(
|
||||
'mfem-backend-benchmark',
|
||||
'backend_operator.cpp',
|
||||
dependencies: [mfem_dep, template_include_dep] + benchmark_accelerator_deps,
|
||||
override_options: ['cpp_std=' + mfem_consumer_cpp_std],
|
||||
build_rpath: benchmark_build_rpath,
|
||||
install_rpath: benchmark_install_rpath,
|
||||
install: true,
|
||||
install_tag: 'benchmarks',
|
||||
)
|
||||
|
||||
if mpi_launcher_from_dependency and not wheel_carries_native_bundle
|
||||
benchmark_launcher_program = find_program(
|
||||
dependency_prefix / 'bin' / 'mpiexec',
|
||||
dependency_prefix / 'bin' / 'mpirun',
|
||||
required: true,
|
||||
)
|
||||
benchmark_mpi_launcher = benchmark_launcher_program.full_path()
|
||||
elif mfem_runtime_prefix != ''
|
||||
benchmark_mpi_launcher = mfem_runtime_prefix / 'bin' / 'mpiexec'
|
||||
else
|
||||
benchmark_launcher_program = find_program('mpiexec', 'mpirun', required: true)
|
||||
benchmark_mpi_launcher = benchmark_launcher_program.full_path()
|
||||
endif
|
||||
|
||||
benchmark_cases = ['cpu']
|
||||
if mfem_features.get('ceed')
|
||||
benchmark_cases += ['ceed-cpu']
|
||||
endif
|
||||
if mfem_has_openmp
|
||||
benchmark_cases += ['omp']
|
||||
endif
|
||||
benchmark_cases += ['mpi']
|
||||
if mfem_has_cuda
|
||||
benchmark_cases += ['cuda']
|
||||
if mfem_features.get('ceed')
|
||||
benchmark_cases += ['ceed-cuda']
|
||||
endif
|
||||
endif
|
||||
|
||||
benchmark_environment = environment()
|
||||
if (
|
||||
mfem_runtime_prefix != '' and dependency_prefix != '' and
|
||||
not wheel_carries_native_bundle
|
||||
)
|
||||
benchmark_environment.prepend('PATH', dependency_prefix / 'bin')
|
||||
if host_machine.system() == 'darwin'
|
||||
benchmark_environment.prepend('DYLD_LIBRARY_PATH', dependency_prefix / 'lib64')
|
||||
benchmark_environment.prepend('DYLD_LIBRARY_PATH', dependency_prefix / 'lib')
|
||||
elif not is_windows
|
||||
benchmark_environment.prepend('LD_LIBRARY_PATH', dependency_prefix / 'lib64')
|
||||
benchmark_environment.prepend('LD_LIBRARY_PATH', dependency_prefix / 'lib')
|
||||
endif
|
||||
endif
|
||||
if mfem_runtime_prefix != ''
|
||||
benchmark_environment.prepend('PATH', mfem_runtime_prefix / 'bin')
|
||||
if host_machine.system() == 'darwin'
|
||||
benchmark_environment.prepend('DYLD_LIBRARY_PATH', mfem_runtime_prefix / 'lib64')
|
||||
benchmark_environment.prepend('DYLD_LIBRARY_PATH', mfem_runtime_prefix / 'lib')
|
||||
elif not is_windows
|
||||
benchmark_environment.prepend('LD_LIBRARY_PATH', mfem_runtime_prefix / 'lib64')
|
||||
benchmark_environment.prepend('LD_LIBRARY_PATH', mfem_runtime_prefix / 'lib')
|
||||
endif
|
||||
endif
|
||||
|
||||
benchmark(
|
||||
'mfem-backend-comparison',
|
||||
python_build,
|
||||
args: [
|
||||
files('../tools/run_backend_benchmarks.py'),
|
||||
'--executable', benchmark_executable,
|
||||
'--launcher', benchmark_mpi_launcher,
|
||||
'--cases', ','.join(benchmark_cases),
|
||||
'--output-dir', meson.current_build_dir(),
|
||||
],
|
||||
depends: benchmark_executable,
|
||||
env: benchmark_environment,
|
||||
timeout: 3600,
|
||||
)
|
||||
endif
|
||||
@@ -82,8 +82,8 @@ source_capable = {
|
||||
'fms': not is_windows,
|
||||
'ceed': not is_windows,
|
||||
'algoim': not meson.is_cross_build() and not is_windows and not is_wasm,
|
||||
'cuda': cuda_toolkit_available and not is_wasm and not host_machine.system() == 'darwin',
|
||||
'hip': hip_toolkit_available and not is_wasm and not host_machine.system() == 'darwin',
|
||||
'cuda': cuda_toolkit_available and not is_wasm and host_machine.system() != 'darwin',
|
||||
'hip': hip_toolkit_available and not is_wasm and host_machine.system() != 'darwin',
|
||||
'simd': simd_arch_supported and not is_wasm,
|
||||
}
|
||||
|
||||
@@ -567,6 +567,23 @@ else
|
||||
'-L' + mfem_runtime_prefix / 'lib',
|
||||
'-L' + mfem_runtime_prefix / 'lib64',
|
||||
]
|
||||
if mfem_has_cuda
|
||||
cuda_compiler_path = dependency_has_nvcc ? (
|
||||
dependency_prefix / 'bin' / 'nvcc'
|
||||
) : nvcc_program.full_path()
|
||||
cuda_root_result = run_command(
|
||||
python_build,
|
||||
'-c',
|
||||
'import os, sys; print(os.path.dirname(os.path.dirname(os.path.realpath(sys.argv[1]))))',
|
||||
cuda_compiler_path,
|
||||
check: true,
|
||||
)
|
||||
cuda_root = cuda_root_result.stdout().strip()
|
||||
if not fs.exists(cuda_root / 'include' / 'cusparse.h')
|
||||
error('CUDA public headers were not found under ' + cuda_root + '/include.')
|
||||
endif
|
||||
bundle_compile_args += ['-I' + cuda_root / 'include']
|
||||
endif
|
||||
if dependency_prefix != '' and not wheel_carries_native_bundle
|
||||
bundle_compile_args += ['-I' + dependency_prefix / 'include']
|
||||
bundle_link_args += [
|
||||
@@ -585,7 +602,9 @@ else
|
||||
bundle_link_args += ['-lgs']
|
||||
endif
|
||||
if mfem_has_zlib
|
||||
bundle_link_args += ['-lz']
|
||||
bundle_link_args += is_wasm ? [
|
||||
mfem_runtime_prefix / 'lib' / 'libz.a',
|
||||
] : ['-lz']
|
||||
endif
|
||||
if mfem_has_sundials
|
||||
bundle_link_args += [
|
||||
|
||||
@@ -13,12 +13,19 @@ int main(int argc, char **argv)
|
||||
mfem::Mpi::Init(argc, argv);
|
||||
mfem::Hypre::Init();
|
||||
const int rank = mfem::Mpi::WorldRank();
|
||||
const int ranks = mfem::Mpi::WorldSize();
|
||||
const char *device_name = argc > 1 ? argv[1] : "cpu";
|
||||
mfem::Device device(device_name);
|
||||
|
||||
mfem::Mesh serial_mesh = mfem::Mesh::MakeCartesian2D(
|
||||
8, 8, mfem::Element::QUADRILATERAL, true, 1.0, 1.0);
|
||||
mfem::ParMesh mesh(MPI_COMM_WORLD, serial_mesh);
|
||||
mfem::Array<int> partitioning(serial_mesh.GetNE());
|
||||
for (int element = 0; element < serial_mesh.GetNE(); ++element)
|
||||
{
|
||||
partitioning[element] = static_cast<long long>(element) * ranks /
|
||||
serial_mesh.GetNE();
|
||||
}
|
||||
mfem::ParMesh mesh(MPI_COMM_WORLD, serial_mesh, partitioning.GetData());
|
||||
serial_mesh.Clear();
|
||||
|
||||
mfem::H1_FECollection elements(2, mesh.Dimension());
|
||||
@@ -66,10 +73,11 @@ int main(int argc, char **argv)
|
||||
|
||||
const double local_norm = solution.Norml2();
|
||||
double global_norm = 0.0;
|
||||
MPI_Allreduce(&local_norm, &global_norm, 1, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);
|
||||
MPI_Allreduce(&local_norm, &global_norm, 1, MPI_DOUBLE, MPI_SUM,
|
||||
MPI_COMM_WORLD);
|
||||
if (rank == 0)
|
||||
{
|
||||
std::cout << "MFEM parallel Poisson: ranks=" << mfem::Mpi::WorldSize()
|
||||
std::cout << "MFEM parallel Poisson: ranks=" << ranks
|
||||
<< " global_dofs=" << space.GlobalTrueVSize()
|
||||
<< " norm_sum=" << global_norm
|
||||
<< " device=" << device_name << '\n';
|
||||
|
||||
177
justfile
177
justfile
@@ -5,6 +5,8 @@ python_bin := env_var_or_default("PYTHON", "python3")
|
||||
jobs := env_var_or_default("MFEM_JOBS", "4")
|
||||
allow_preinstalled := env_var_or_default("MFEM_ALLOW_PREINSTALLED", "false")
|
||||
dependency_prefix := env_var_or_default("MFEM_DEPENDENCY_PREFIX", "")
|
||||
cuda_home := env_var_or_default("CUDA_HOME", "")
|
||||
cuda_arch := env_var_or_default("MFEM_CUDA_ARCH", "native")
|
||||
web_port := env_var_or_default("MFEM_WEB_PORT", "8000")
|
||||
macos_target := env_var_or_default("MACOSX_DEPLOYMENT_TARGET", "11.0")
|
||||
|
||||
@@ -25,6 +27,14 @@ parallel_install := install_root + "/parallel"
|
||||
debug_install := install_root + "/debug"
|
||||
cuda_install := install_root + "/cuda"
|
||||
hip_install := install_root + "/hip"
|
||||
benchmark_trials := env_var_or_default("MFEM_BENCH_TRIALS", "5")
|
||||
benchmark_mesh_n := env_var_or_default("MFEM_BENCH_MESH_N", "48")
|
||||
benchmark_order := env_var_or_default("MFEM_BENCH_ORDER", "3")
|
||||
benchmark_applications := env_var_or_default("MFEM_BENCH_APPLICATIONS", "50")
|
||||
benchmark_minimum_apply_seconds := env_var_or_default("MFEM_BENCH_MIN_SECONDS", "1.0")
|
||||
benchmark_max_applications := env_var_or_default("MFEM_BENCH_MAX_APPLICATIONS", "1000000")
|
||||
benchmark_omp_threads := env_var_or_default("MFEM_BENCH_OMP_THREADS", "8")
|
||||
benchmark_mpi_ranks := env_var_or_default("MFEM_BENCH_MPI_RANKS", "4")
|
||||
|
||||
default:
|
||||
@just --list
|
||||
@@ -58,11 +68,65 @@ basic: (configure basic_dir basic_install "release" "minimal" allow_preinstalled
|
||||
|
||||
serial: (configure serial_dir serial_install "release" "portable" allow_preinstalled "-Dmfem_mpi=disabled" "-Dmfem_metis=disabled" "-Dmfem_cuda=disabled" "-Dmfem_hip=disabled") (build-and-test serial_dir)
|
||||
|
||||
parallel: (configure parallel_dir parallel_install "release" "portable" allow_preinstalled "-Dmfem_mpi=enabled" "-Dmfem_sundials=enabled") (build-and-test parallel_dir)
|
||||
parallel: (configure parallel_dir parallel_install "release" "portable" allow_preinstalled "-Dmfem_mpi=enabled" "-Dmfem_sundials=enabled" "-Dmfem_cuda=disabled" "-Dmfem_hip=disabled" "-Dbuild_benchmarks=true") (build-and-test parallel_dir)
|
||||
|
||||
debug: (configure debug_dir debug_install "debug" "portable" allow_preinstalled) (build-and-test debug_dir)
|
||||
|
||||
cuda: (configure cuda_dir cuda_install "release" "portable" allow_preinstalled "-Dmfem_cuda=enabled" "-Dmfem_hip=disabled") (build-and-test cuda_dir)
|
||||
cuda:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cuda_root="{{ cuda_home }}"
|
||||
if [[ -z "$cuda_root" ]]; then
|
||||
if command -v nvcc >/dev/null 2>&1; then
|
||||
nvcc_path="$(command -v nvcc)"
|
||||
while [[ -L "$nvcc_path" ]]; do
|
||||
nvcc_target="$(readlink "$nvcc_path")"
|
||||
if [[ "$nvcc_target" == /* ]]; then
|
||||
nvcc_path="$nvcc_target"
|
||||
else
|
||||
nvcc_path="$(dirname "$nvcc_path")/$nvcc_target"
|
||||
fi
|
||||
done
|
||||
cuda_root="$(cd "$(dirname "$nvcc_path")/.." && pwd -P)"
|
||||
elif [[ -x /usr/local/cuda/bin/nvcc ]]; then
|
||||
cuda_root=/usr/local/cuda
|
||||
fi
|
||||
fi
|
||||
if [[ -n "$cuda_root" ]]; then
|
||||
export CUDA_HOME="$cuda_root"
|
||||
export PATH="$cuda_root/bin:$PATH"
|
||||
fi
|
||||
if ! command -v nvcc >/dev/null 2>&1; then
|
||||
echo "CUDA was requested, but nvcc was not found; set CUDA_HOME or PATH." >&2
|
||||
exit 2
|
||||
fi
|
||||
selected_arch="{{ cuda_arch }}"
|
||||
if [[ "$selected_arch" == native ]] && command -v nvidia-smi >/dev/null 2>&1; then
|
||||
if detected_arch="$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader | sed -n '1{s/[[:space:].]//g;p;}')" && [[ "$detected_arch" =~ ^[0-9]+$ ]]; then
|
||||
selected_arch="$detected_arch"
|
||||
fi
|
||||
fi
|
||||
setup_args=(
|
||||
"{{ cuda_dir }}"
|
||||
"--prefix={{ cuda_install }}"
|
||||
"--buildtype=release"
|
||||
"-Dfeature_profile=portable"
|
||||
"-Dallow_preinstalled={{ allow_preinstalled }}"
|
||||
"-Ddependency_prefix={{ dependency_prefix }}"
|
||||
"-Djobs={{ jobs }}"
|
||||
"-Dbuild_examples=true"
|
||||
"-Dbuild_benchmarks=true"
|
||||
"-Dbuild_tests=true"
|
||||
"-Dmfem_cuda=enabled"
|
||||
"-Dmfem_hip=disabled"
|
||||
"-Dmfem_cuda_arch=${selected_arch}"
|
||||
)
|
||||
if [[ -f "{{ cuda_dir }}/meson-private/coredata.dat" ]]; then
|
||||
setup_args=(--reconfigure "${setup_args[@]}")
|
||||
fi
|
||||
"{{ meson_bin }}" setup "${setup_args[@]}"
|
||||
"{{ meson_bin }}" compile -C "{{ cuda_dir }}"
|
||||
"{{ meson_bin }}" test -C "{{ cuda_dir }}" --print-errorlogs
|
||||
|
||||
hip: (configure hip_dir hip_install "release" "portable" allow_preinstalled "-Dmfem_hip=enabled" "-Dmfem_cuda=disabled") (build-and-test hip_dir)
|
||||
|
||||
@@ -103,6 +167,115 @@ run-serial: serial
|
||||
run-parallel ranks="4": parallel
|
||||
"{{ python_bin }}" "{{ project_root }}/tools/run_mpi_test.py" --launcher "{{ parallel_dir }}/build-config/mfem/mfem-prefix/bin/mpiexec" --processes "{{ ranks }}" "{{ parallel_dir }}/examples/mfem-parallel-hypre"
|
||||
|
||||
run-cuda ranks="2": cuda
|
||||
"{{ python_bin }}" "{{ project_root }}/tools/run_mpi_test.py" --launcher "{{ cuda_dir }}/build-config/mfem/mfem-prefix/bin/mpiexec" --processes "{{ ranks }}" "{{ cuda_dir }}/examples/mfem-parallel-hypre" cuda
|
||||
|
||||
benchmark: benchmark-cpu
|
||||
|
||||
benchmark-cpu: parallel
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
launcher="{{ parallel_dir }}/build-config/mfem/mfem-prefix/bin/mpiexec"
|
||||
dep_prefix="{{ dependency_prefix }}"
|
||||
if [[ ! -x "$launcher" && -n "$dep_prefix" ]]; then
|
||||
for candidate in "$dep_prefix/bin/mpiexec" "$dep_prefix/bin/mpirun"; do
|
||||
[[ -x "$candidate" ]] && launcher="$candidate" && break
|
||||
done
|
||||
fi
|
||||
if [[ ! -x "$launcher" ]]; then
|
||||
launcher="$(command -v mpiexec || command -v mpirun || true)"
|
||||
fi
|
||||
[[ -n "$launcher" ]] || { echo "No MPI launcher was found." >&2; exit 2; }
|
||||
if [[ -n "$dep_prefix" ]]; then
|
||||
export PATH="$dep_prefix/bin:$PATH"
|
||||
if [[ "$(uname -s)" == Darwin ]]; then
|
||||
export DYLD_LIBRARY_PATH="$dep_prefix/lib:$dep_prefix/lib64:${DYLD_LIBRARY_PATH:-}"
|
||||
else
|
||||
export LD_LIBRARY_PATH="$dep_prefix/lib:$dep_prefix/lib64:${LD_LIBRARY_PATH:-}"
|
||||
fi
|
||||
fi
|
||||
"{{ python_bin }}" "{{ project_root }}/tools/run_backend_benchmarks.py" \
|
||||
--executable "{{ parallel_dir }}/benchmarks/mfem-backend-benchmark" \
|
||||
--launcher "$launcher" \
|
||||
--cases cpu,ceed-cpu,omp,mpi \
|
||||
--omp-threads "{{ benchmark_omp_threads }}" \
|
||||
--mpi-ranks "{{ benchmark_mpi_ranks }}" \
|
||||
--trials "{{ benchmark_trials }}" \
|
||||
--mesh-n "{{ benchmark_mesh_n }}" \
|
||||
--order "{{ benchmark_order }}" \
|
||||
--applications "{{ benchmark_applications }}" \
|
||||
--minimum-apply-seconds "{{ benchmark_minimum_apply_seconds }}" \
|
||||
--max-applications "{{ benchmark_max_applications }}" \
|
||||
--output-dir "{{ parallel_dir }}/benchmarks/results"
|
||||
|
||||
benchmark-cuda: cuda
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
launcher="{{ cuda_dir }}/build-config/mfem/mfem-prefix/bin/mpiexec"
|
||||
dep_prefix="{{ dependency_prefix }}"
|
||||
if [[ ! -x "$launcher" && -n "$dep_prefix" ]]; then
|
||||
for candidate in "$dep_prefix/bin/mpiexec" "$dep_prefix/bin/mpirun"; do
|
||||
[[ -x "$candidate" ]] && launcher="$candidate" && break
|
||||
done
|
||||
fi
|
||||
if [[ ! -x "$launcher" ]]; then
|
||||
launcher="$(command -v mpiexec || command -v mpirun || true)"
|
||||
fi
|
||||
[[ -n "$launcher" ]] || { echo "No MPI launcher was found." >&2; exit 2; }
|
||||
if [[ -n "$dep_prefix" ]]; then
|
||||
export PATH="$dep_prefix/bin:$PATH"
|
||||
if [[ "$(uname -s)" == Darwin ]]; then
|
||||
export DYLD_LIBRARY_PATH="$dep_prefix/lib:$dep_prefix/lib64:${DYLD_LIBRARY_PATH:-}"
|
||||
else
|
||||
export LD_LIBRARY_PATH="$dep_prefix/lib:$dep_prefix/lib64:${LD_LIBRARY_PATH:-}"
|
||||
fi
|
||||
fi
|
||||
"{{ python_bin }}" "{{ project_root }}/tools/run_backend_benchmarks.py" \
|
||||
--executable "{{ cuda_dir }}/benchmarks/mfem-backend-benchmark" \
|
||||
--launcher "$launcher" \
|
||||
--cases cpu,ceed-cpu,omp,mpi,cuda,ceed-cuda \
|
||||
--omp-threads "{{ benchmark_omp_threads }}" \
|
||||
--mpi-ranks "{{ benchmark_mpi_ranks }}" \
|
||||
--trials "{{ benchmark_trials }}" \
|
||||
--mesh-n "{{ benchmark_mesh_n }}" \
|
||||
--order "{{ benchmark_order }}" \
|
||||
--applications "{{ benchmark_applications }}" \
|
||||
--minimum-apply-seconds "{{ benchmark_minimum_apply_seconds }}" \
|
||||
--max-applications "{{ benchmark_max_applications }}" \
|
||||
--output-dir "{{ cuda_dir }}/benchmarks/results"
|
||||
|
||||
benchmark-quick: parallel
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
launcher="{{ parallel_dir }}/build-config/mfem/mfem-prefix/bin/mpiexec"
|
||||
dep_prefix="{{ dependency_prefix }}"
|
||||
if [[ ! -x "$launcher" && -n "$dep_prefix" ]]; then
|
||||
for candidate in "$dep_prefix/bin/mpiexec" "$dep_prefix/bin/mpirun"; do
|
||||
[[ -x "$candidate" ]] && launcher="$candidate" && break
|
||||
done
|
||||
fi
|
||||
if [[ ! -x "$launcher" ]]; then
|
||||
launcher="$(command -v mpiexec || command -v mpirun || true)"
|
||||
fi
|
||||
[[ -n "$launcher" ]] || { echo "No MPI launcher was found." >&2; exit 2; }
|
||||
if [[ -n "$dep_prefix" ]]; then
|
||||
export PATH="$dep_prefix/bin:$PATH"
|
||||
if [[ "$(uname -s)" == Darwin ]]; then
|
||||
export DYLD_LIBRARY_PATH="$dep_prefix/lib:$dep_prefix/lib64:${DYLD_LIBRARY_PATH:-}"
|
||||
else
|
||||
export LD_LIBRARY_PATH="$dep_prefix/lib:$dep_prefix/lib64:${LD_LIBRARY_PATH:-}"
|
||||
fi
|
||||
fi
|
||||
"{{ python_bin }}" "{{ project_root }}/tools/run_backend_benchmarks.py" \
|
||||
--executable "{{ parallel_dir }}/benchmarks/mfem-backend-benchmark" \
|
||||
--launcher "$launcher" \
|
||||
--cases cpu,ceed-cpu,omp,mpi \
|
||||
--omp-threads "{{ benchmark_omp_threads }}" \
|
||||
--mpi-ranks "{{ benchmark_mpi_ranks }}" \
|
||||
--trials 1 --mesh-n 16 --order 2 --applications 5 \
|
||||
--minimum-apply-seconds 0 \
|
||||
--output-dir "{{ parallel_dir }}/benchmarks/quick-results"
|
||||
|
||||
install-basic: basic
|
||||
"{{ meson_bin }}" install -C "{{ basic_dir }}"
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ subdir('build-check')
|
||||
subdir('build-config')
|
||||
subdir('include/meson_mfem_template')
|
||||
subdir('examples')
|
||||
subdir('benchmarks')
|
||||
subdir('web')
|
||||
subdir('build-python')
|
||||
subdir('tests')
|
||||
@@ -52,6 +53,7 @@ summary(
|
||||
'Algoim': mfem_features.get('algoim'),
|
||||
'Python bindings': get_option('build_python'),
|
||||
'examples': get_option('build_examples'),
|
||||
'benchmarks': get_option('build_benchmarks'),
|
||||
'tests': get_option('build_tests'),
|
||||
},
|
||||
section: 'meson-mfem-template ' + meson.project_version(),
|
||||
|
||||
@@ -2,6 +2,7 @@ option('allow_preinstalled', type: 'boolean', value: true, description: 'Allow o
|
||||
option('feature_profile', type: 'combo', choices: ['minimal', 'portable', 'full'], value: 'portable', description: 'Defaults for auto features. full is strict and requires all selected non-bundled TPLs in dependency_prefix.')
|
||||
option('build_python', type: 'boolean', value: false, description: 'Build the nanobind Python extension and install native assets inside the Python package.')
|
||||
option('build_examples', type: 'boolean', value: true, description: 'Build MFEM capability examples.')
|
||||
option('build_benchmarks', type: 'boolean', value: false, description: 'Build the native MPI-capable backend benchmark and register the benchmark suite.')
|
||||
option('build_tests', type: 'boolean', value: true, description: 'Register serial, parallel, and Python smoke tests when applicable.')
|
||||
option('install_mfem', type: 'boolean', value: true, description: 'Install the source-built MFEM bundle and its runtime dependencies.')
|
||||
option('dependency_prefix', type: 'string', value: '', description: 'Explicit prefix containing optional TPLs. Used even when allow_preinstalled=false.')
|
||||
|
||||
@@ -31,6 +31,16 @@ if get_option('build_tests') and get_option('build_examples')
|
||||
timeout: 120,
|
||||
)
|
||||
|
||||
if mfem_has_cuda
|
||||
test(
|
||||
'serial-poisson-cuda',
|
||||
serial_example,
|
||||
args: ['cuda'],
|
||||
env: runtime_environment,
|
||||
timeout: 120,
|
||||
)
|
||||
endif
|
||||
|
||||
if mfem_has_mpi
|
||||
if mpi_launcher_from_dependency and not wheel_carries_native_bundle
|
||||
mpi_launcher_program = find_program(
|
||||
@@ -57,9 +67,76 @@ if get_option('build_tests') and get_option('build_examples')
|
||||
env: runtime_environment,
|
||||
timeout: 180,
|
||||
)
|
||||
if mfem_has_cuda
|
||||
test(
|
||||
'parallel-hypre-boomeramg-cuda',
|
||||
python_build,
|
||||
args: [
|
||||
files('../tools/run_mpi_test.py'),
|
||||
'--launcher', mpi_launcher,
|
||||
'--processes', '2',
|
||||
parallel_example,
|
||||
'cuda',
|
||||
],
|
||||
env: runtime_environment,
|
||||
timeout: 180,
|
||||
)
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
if get_option('build_tests') and get_option('build_benchmarks')
|
||||
benchmark_smoke_environment = environment()
|
||||
benchmark_smoke_environment.set('OMP_NUM_THREADS', '1')
|
||||
benchmark_smoke_environment.set('OMP_DYNAMIC', 'FALSE')
|
||||
if (
|
||||
mfem_runtime_prefix != '' and dependency_prefix != '' and
|
||||
not wheel_carries_native_bundle
|
||||
)
|
||||
benchmark_smoke_environment.prepend('PATH', dependency_prefix / 'bin')
|
||||
if host_machine.system() == 'darwin'
|
||||
benchmark_smoke_environment.prepend('DYLD_LIBRARY_PATH', dependency_prefix / 'lib64')
|
||||
benchmark_smoke_environment.prepend('DYLD_LIBRARY_PATH', dependency_prefix / 'lib')
|
||||
elif host_machine.system() != 'windows' and not is_wasm
|
||||
benchmark_smoke_environment.prepend('LD_LIBRARY_PATH', dependency_prefix / 'lib64')
|
||||
benchmark_smoke_environment.prepend('LD_LIBRARY_PATH', dependency_prefix / 'lib')
|
||||
endif
|
||||
endif
|
||||
if mfem_runtime_prefix != ''
|
||||
benchmark_smoke_environment.prepend('PATH', mfem_runtime_prefix / 'bin')
|
||||
if host_machine.system() == 'darwin'
|
||||
benchmark_smoke_environment.prepend('DYLD_LIBRARY_PATH', mfem_runtime_prefix / 'lib64')
|
||||
benchmark_smoke_environment.prepend('DYLD_LIBRARY_PATH', mfem_runtime_prefix / 'lib')
|
||||
elif host_machine.system() != 'windows'
|
||||
benchmark_smoke_environment.prepend('LD_LIBRARY_PATH', mfem_runtime_prefix / 'lib64')
|
||||
benchmark_smoke_environment.prepend('LD_LIBRARY_PATH', mfem_runtime_prefix / 'lib')
|
||||
endif
|
||||
endif
|
||||
test(
|
||||
'backend-benchmark-smoke',
|
||||
python_build,
|
||||
args: [
|
||||
files('../tools/run_mpi_test.py'),
|
||||
'--launcher', benchmark_mpi_launcher,
|
||||
'--processes', '1',
|
||||
'--',
|
||||
benchmark_executable,
|
||||
'--device', 'cpu',
|
||||
'--mesh-n', '4',
|
||||
'--order', '2',
|
||||
'--applications', '2',
|
||||
'--minimum-apply-seconds', '0',
|
||||
'--warmup-applications', '1',
|
||||
'--relative-tolerance', '1e-6',
|
||||
'--max-iterations', '200',
|
||||
'--solve',
|
||||
],
|
||||
depends: benchmark_executable,
|
||||
env: benchmark_smoke_environment,
|
||||
timeout: 180,
|
||||
)
|
||||
endif
|
||||
|
||||
if get_option('build_tests') and get_option('build_python')
|
||||
python_test_environment = environment()
|
||||
python_test_environment.prepend('PYTHONPATH', python_extension_dir)
|
||||
|
||||
@@ -589,15 +589,7 @@ def build_zlib(
|
||||
build.mkdir(parents=True, exist_ok=True)
|
||||
cmake_configure(args, args.zlib_source, build, options, env)
|
||||
run(
|
||||
[
|
||||
args.cmake,
|
||||
"--build",
|
||||
build,
|
||||
"--target",
|
||||
"zlibstatic",
|
||||
"--parallel",
|
||||
str(args.jobs),
|
||||
],
|
||||
[args.cmake, "--build", build, "--parallel", str(args.jobs)],
|
||||
env=env,
|
||||
)
|
||||
run([args.cmake, "--install", build], env=env)
|
||||
@@ -658,7 +650,7 @@ def build_hypre(
|
||||
options = common_cmake_args(
|
||||
args,
|
||||
build_type,
|
||||
c_compiler=mpicc if features["mpi"] else args.cc,
|
||||
c_compiler=args.cc,
|
||||
) + [
|
||||
f"-DHYPRE_ENABLE_MPI={'ON' if features['mpi'] else 'OFF'}",
|
||||
f"-DHYPRE_ENABLE_OPENMP={'ON' if features['openmp'] else 'OFF'}",
|
||||
@@ -668,6 +660,12 @@ def build_hypre(
|
||||
"-DHYPRE_BUILD_EXAMPLES=OFF",
|
||||
"-DHYPRE_BUILD_TESTS=OFF",
|
||||
]
|
||||
if features["mpi"]:
|
||||
# Keep CMake's compiler identity on the host compiler and let FindMPI
|
||||
# interrogate the wrapper. Treating mpicc itself as CMAKE_C_COMPILER
|
||||
# makes MPI's include path look implicit, so CUDA translation units
|
||||
# compiled by nvcc do not receive mpi.h.
|
||||
options.append(f"-DMPI_C_COMPILER={mpicc}")
|
||||
if features["cuda"]:
|
||||
options.extend(
|
||||
[
|
||||
@@ -1336,7 +1334,10 @@ def build_mfem(
|
||||
f"-DGSLIB_DIR={bundle_root(args, 'include/gslib/gslib.h')}"
|
||||
)
|
||||
if features["zlib"]:
|
||||
options.append(f"-DZLIB_ROOT={bundle_root(args, 'include/zlib.h')}")
|
||||
zlib_root = bundle_root(args, "include/zlib.h")
|
||||
options.append(f"-DZLIB_ROOT={zlib_root}")
|
||||
if args.host_system == "emscripten":
|
||||
options.append(f"-DZLIB_LIBRARY={zlib_root / 'lib' / 'libz.a'}")
|
||||
if features["sundials"]:
|
||||
options.append(
|
||||
f"-DSUNDIALS_DIR={bundle_root(args, 'include/sundials/sundials_config.h')}"
|
||||
|
||||
667
tools/run_backend_benchmarks.py
Executable file
667
tools/run_backend_benchmarks.py
Executable file
@@ -0,0 +1,667 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import datetime as dt
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import random
|
||||
import shutil
|
||||
import statistics
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
JSON_PREFIX = "MFEM_BENCHMARK_JSON "
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Case:
|
||||
name: str
|
||||
device: str
|
||||
ranks: int
|
||||
threads: int
|
||||
|
||||
|
||||
def positive_int(value: str) -> int:
|
||||
parsed = int(value)
|
||||
if parsed < 1:
|
||||
raise argparse.ArgumentTypeError("must be at least one")
|
||||
return parsed
|
||||
|
||||
|
||||
def integer_list(value: str) -> list[int]:
|
||||
try:
|
||||
parsed = [int(item.strip()) for item in value.split(",") if item.strip()]
|
||||
except ValueError as error:
|
||||
raise argparse.ArgumentTypeError("expected comma-separated integers") from error
|
||||
if not parsed or any(item < 1 for item in parsed):
|
||||
raise argparse.ArgumentTypeError("all entries must be positive")
|
||||
return parsed
|
||||
|
||||
|
||||
def command_output(command: list[str]) -> str | None:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return None
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
output = result.stdout.strip()
|
||||
return output if output else None
|
||||
|
||||
|
||||
def resolve_program(value: str) -> str | None:
|
||||
candidate = pathlib.Path(value)
|
||||
if candidate.is_file():
|
||||
return str(candidate.resolve())
|
||||
discovered = shutil.which(value)
|
||||
return str(pathlib.Path(discovered).resolve()) if discovered else None
|
||||
|
||||
|
||||
def compact_cpu_list(cpus: list[int]) -> str:
|
||||
if not cpus:
|
||||
return ""
|
||||
groups: list[str] = []
|
||||
start = previous = cpus[0]
|
||||
for current in cpus[1:]:
|
||||
if current == previous + 1:
|
||||
previous = current
|
||||
continue
|
||||
groups.append(str(start) if start == previous else f"{start}-{previous}")
|
||||
start = previous = current
|
||||
groups.append(str(start) if start == previous else f"{start}-{previous}")
|
||||
return ",".join(groups)
|
||||
|
||||
|
||||
def affinity_prefix(case: Case, setting: str) -> tuple[list[str], str | None]:
|
||||
if setting == "none" or shutil.which("taskset") is None:
|
||||
return [], None
|
||||
if setting != "auto":
|
||||
return ["taskset", "-c", setting], setting
|
||||
if not hasattr(os, "sched_getaffinity"):
|
||||
return [], None
|
||||
available = sorted(os.sched_getaffinity(0))
|
||||
count = min(len(available), max(case.ranks, case.threads))
|
||||
chosen = compact_cpu_list(available[:count])
|
||||
return (["taskset", "-c", chosen] if chosen else []), chosen or None
|
||||
|
||||
|
||||
def expand_cases(args: argparse.Namespace) -> list[Case]:
|
||||
requested = [item.strip() for item in args.cases.split(",") if item.strip()]
|
||||
valid = {"cpu", "ceed-cpu", "omp", "mpi", "cuda", "ceed-cuda"}
|
||||
unknown = sorted(set(requested) - valid)
|
||||
if unknown:
|
||||
raise ValueError(f"unknown benchmark case(s): {', '.join(unknown)}")
|
||||
cases: list[Case] = []
|
||||
for requested_case in requested:
|
||||
if requested_case == "cpu":
|
||||
cases.append(Case("cpu-1r-1t", "cpu", 1, 1))
|
||||
elif requested_case == "ceed-cpu":
|
||||
cases.append(Case("ceed-cpu-1r-1t", args.ceed_cpu_device, 1, 1))
|
||||
elif requested_case == "omp":
|
||||
cases.extend(
|
||||
Case(f"omp-1r-{threads}t", "omp", 1, threads)
|
||||
for threads in args.omp_threads
|
||||
)
|
||||
elif requested_case == "mpi":
|
||||
cases.extend(
|
||||
Case(f"mpi-{ranks}r-1t", "cpu", ranks, 1)
|
||||
for ranks in args.mpi_ranks
|
||||
)
|
||||
elif requested_case == "cuda":
|
||||
cases.append(Case("cuda-1r-1t", "cuda", 1, 1))
|
||||
elif requested_case == "ceed-cuda":
|
||||
cases.append(Case("ceed-cuda-1r-1t", args.ceed_cuda_device, 1, 1))
|
||||
deduplicated: list[Case] = []
|
||||
for case in cases:
|
||||
if case not in deduplicated:
|
||||
deduplicated.append(case)
|
||||
return deduplicated
|
||||
|
||||
|
||||
def run_case(
|
||||
args: argparse.Namespace, case: Case, trial: int, warmup_trial: bool
|
||||
) -> dict[str, Any]:
|
||||
prefix, affinity = affinity_prefix(case, args.affinity)
|
||||
command = prefix + [args.launcher, "-n", str(case.ranks)]
|
||||
command.extend(args.launcher_arg)
|
||||
command.extend(
|
||||
[
|
||||
args.executable,
|
||||
"--device",
|
||||
case.device,
|
||||
"--mesh-n",
|
||||
str(args.mesh_n),
|
||||
"--order",
|
||||
str(args.order),
|
||||
"--applications",
|
||||
str(args.applications),
|
||||
"--max-applications",
|
||||
str(args.max_applications),
|
||||
"--minimum-apply-seconds",
|
||||
str(args.minimum_apply_seconds),
|
||||
"--warmup-applications",
|
||||
str(args.warmup_applications),
|
||||
"--relative-tolerance",
|
||||
str(args.relative_tolerance),
|
||||
"--max-iterations",
|
||||
str(args.max_iterations),
|
||||
"--trial",
|
||||
str(trial),
|
||||
]
|
||||
)
|
||||
if args.solve:
|
||||
command.append("--solve")
|
||||
environment = os.environ.copy()
|
||||
environment.update(
|
||||
{
|
||||
"OMP_NUM_THREADS": str(case.threads),
|
||||
"OMP_DYNAMIC": "FALSE",
|
||||
"OMP_PROC_BIND": "close",
|
||||
"OMP_PLACES": "cores",
|
||||
"FI_PROVIDER": environment.get("FI_PROVIDER", "sockets"),
|
||||
}
|
||||
)
|
||||
environment.setdefault("OMPI_ALLOW_RUN_AS_ROOT", "1")
|
||||
environment.setdefault("OMPI_ALLOW_RUN_AS_ROOT_CONFIRM", "1")
|
||||
selected_interface = args.network_interface
|
||||
if selected_interface == "local":
|
||||
selected_interface = "lo0" if sys.platform == "darwin" else "lo"
|
||||
if selected_interface == "auto":
|
||||
environment.pop("FI_SOCKETS_IFACE", None)
|
||||
environment.pop("HYDRA_IFACE", None)
|
||||
environment.pop("MPICH_INTERFACE_HOSTNAME", None)
|
||||
else:
|
||||
environment["FI_SOCKETS_IFACE"] = selected_interface
|
||||
environment["HYDRA_IFACE"] = selected_interface
|
||||
environment.pop("MPICH_INTERFACE_HOSTNAME", None)
|
||||
if sys.platform == "darwin" and args.network_interface == "local":
|
||||
environment["MPICH_INTERFACE_HOSTNAME"] = "127.0.0.1"
|
||||
if case.device.startswith("cuda") or case.device.startswith("ceed-cuda"):
|
||||
if args.gpu_aware_mpi:
|
||||
environment["MFEM_GPU_AWARE_MPI"] = "1"
|
||||
else:
|
||||
environment.pop("MFEM_GPU_AWARE_MPI", None)
|
||||
if args.cuda_visible_devices is not None:
|
||||
environment["CUDA_VISIBLE_DEVICES"] = args.cuda_visible_devices
|
||||
|
||||
started = time.perf_counter()
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=environment,
|
||||
timeout=args.timeout,
|
||||
)
|
||||
wall_seconds = time.perf_counter() - started
|
||||
combined_output = "\n".join((completed.stdout, completed.stderr))
|
||||
records = [
|
||||
json.loads(line[len(JSON_PREFIX) :])
|
||||
for line in combined_output.splitlines()
|
||||
if line.startswith(JSON_PREFIX)
|
||||
]
|
||||
if completed.returncode != 0 or len(records) != 1:
|
||||
tail = "\n".join(combined_output.splitlines()[-30:])
|
||||
raise RuntimeError(
|
||||
f"{case.name} trial {trial} failed with exit code "
|
||||
f"{completed.returncode}; JSON records={len(records)}\n{tail}"
|
||||
)
|
||||
record = records[0]
|
||||
record.update(
|
||||
{
|
||||
"case": case.name,
|
||||
"requested_device": case.device,
|
||||
"runner_trial": trial,
|
||||
"discarded_warmup_trial": warmup_trial,
|
||||
"wall_seconds": wall_seconds,
|
||||
"cpu_affinity": affinity,
|
||||
"network_interface": environment.get("HYDRA_IFACE"),
|
||||
"cuda_visible_devices": environment.get("CUDA_VISIBLE_DEVICES"),
|
||||
"gpu_aware_mpi": environment.get("MFEM_GPU_AWARE_MPI") == "1",
|
||||
"command": command,
|
||||
}
|
||||
)
|
||||
if not record.get("valid", False):
|
||||
raise RuntimeError(f"{case.name} trial {trial} failed numerical validation")
|
||||
return record
|
||||
|
||||
|
||||
def median_absolute_deviation(values: list[float]) -> float:
|
||||
center = statistics.median(values)
|
||||
return statistics.median(abs(value - center) for value in values)
|
||||
|
||||
|
||||
def validate_cross_backend(
|
||||
records: list[dict[str, Any]], relative_tolerance: float
|
||||
) -> dict[str, float]:
|
||||
measured = [row for row in records if not row["discarded_warmup_trial"]]
|
||||
baseline = [row for row in measured if row["case"] == "cpu-1r-1t"]
|
||||
if not baseline:
|
||||
return {}
|
||||
validation_fields = ["probe_norm"]
|
||||
if measured and all(row.get("solve_ran", False) for row in measured):
|
||||
validation_fields.append("solution_norm")
|
||||
references = {
|
||||
field: statistics.median(float(row[field]) for row in baseline)
|
||||
for field in validation_fields
|
||||
}
|
||||
maximum_errors = {field: 0.0 for field in references}
|
||||
for row in measured:
|
||||
for field, reference in references.items():
|
||||
relative_error = abs(float(row[field]) - reference) / max(
|
||||
abs(reference), 1.0e-300
|
||||
)
|
||||
maximum_errors[field] = max(maximum_errors[field], relative_error)
|
||||
if relative_error > relative_tolerance:
|
||||
raise RuntimeError(
|
||||
f"{row['case']} disagrees with CPU for {field}: relative "
|
||||
f"error {relative_error:.3e} > {relative_tolerance:.3e}"
|
||||
)
|
||||
return maximum_errors
|
||||
|
||||
|
||||
def checkpoint_results(
|
||||
output_dir: pathlib.Path,
|
||||
metadata: dict[str, Any],
|
||||
records: list[dict[str, Any]],
|
||||
) -> None:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
destination = output_dir / "partial-results.json"
|
||||
temporary = output_dir / "partial-results.json.tmp"
|
||||
temporary.write_text(
|
||||
json.dumps({"schema_version": 1, "metadata": metadata, "raw": records}, indent=2)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
temporary.replace(destination)
|
||||
|
||||
|
||||
def summarize(cases: list[Case], records: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
summaries: list[dict[str, Any]] = []
|
||||
for case in cases:
|
||||
rows = [
|
||||
row
|
||||
for row in records
|
||||
if row["case"] == case.name and not row["discarded_warmup_trial"]
|
||||
]
|
||||
if not rows:
|
||||
continue
|
||||
throughput = [float(row["apply_mdof_per_second"]) for row in rows]
|
||||
apply_seconds = [float(row["apply_seconds"]) for row in rows]
|
||||
solve_seconds = [float(row["solve_seconds"]) for row in rows]
|
||||
total_seconds = [float(row["total_seconds"]) for row in rows]
|
||||
timed_applications = [int(row["applications"]) for row in rows]
|
||||
summaries.append(
|
||||
{
|
||||
"case": case.name,
|
||||
"device": case.device,
|
||||
"ranks": case.ranks,
|
||||
"threads": case.threads,
|
||||
"trials": len(rows),
|
||||
"global_true_dofs": rows[0]["global_true_dofs"],
|
||||
"requested_applications": rows[0]["requested_applications"],
|
||||
"applications_min": min(timed_applications),
|
||||
"applications_max": max(timed_applications),
|
||||
"apply_mdof_per_second_median": statistics.median(throughput),
|
||||
"apply_mdof_per_second_mad": median_absolute_deviation(throughput),
|
||||
"apply_mdof_per_second_min": min(throughput),
|
||||
"apply_mdof_per_second_max": max(throughput),
|
||||
"apply_seconds_median": statistics.median(apply_seconds),
|
||||
"solve_seconds_median": statistics.median(solve_seconds),
|
||||
"total_seconds_median": statistics.median(total_seconds),
|
||||
"cg_iterations": sorted({row["cg_iterations"] for row in rows}),
|
||||
"solve_ran": all(bool(row["solve_ran"]) for row in rows),
|
||||
"verified_relative_residual_max": max(
|
||||
float(row["verified_relative_residual"]) for row in rows
|
||||
),
|
||||
}
|
||||
)
|
||||
baseline = next((row for row in summaries if row["case"] == "cpu-1r-1t"), None)
|
||||
baseline_rate = (
|
||||
float(baseline["apply_mdof_per_second_median"]) if baseline else None
|
||||
)
|
||||
for row in summaries:
|
||||
row["speedup_vs_cpu"] = (
|
||||
float(row["apply_mdof_per_second_median"]) / baseline_rate
|
||||
if baseline_rate
|
||||
else None
|
||||
)
|
||||
return summaries
|
||||
|
||||
|
||||
def markdown_table(summaries: list[dict[str, Any]]) -> str:
|
||||
lines = [
|
||||
"| Case | Device | Ranks | Threads | Timed apps | Apply window (s) | Apply MDoF/s, median [range] | MAD | Speedup | Solve (s) | CG iters |",
|
||||
"|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|",
|
||||
]
|
||||
for row in summaries:
|
||||
speedup = row["speedup_vs_cpu"]
|
||||
speedup_text = f"{speedup:.2f}x" if speedup is not None else "n/a"
|
||||
solve_text = f"{row['solve_seconds_median']:.4f}" if row["solve_ran"] else "—"
|
||||
iteration_text = (
|
||||
",".join(str(value) for value in row["cg_iterations"])
|
||||
if row["solve_ran"]
|
||||
else "—"
|
||||
)
|
||||
applications_text = (
|
||||
str(row["applications_min"])
|
||||
if row["applications_min"] == row["applications_max"]
|
||||
else f"{row['applications_min']}-{row['applications_max']}"
|
||||
)
|
||||
lines.append(
|
||||
f"| {row['case']} | `{row['device']}` | {row['ranks']} | "
|
||||
f"{row['threads']} | {applications_text} | "
|
||||
f"{row['apply_seconds_median']:.3f} | "
|
||||
f"{row['apply_mdof_per_second_median']:.2f} "
|
||||
f"[{row['apply_mdof_per_second_min']:.2f}, "
|
||||
f"{row['apply_mdof_per_second_max']:.2f}] | "
|
||||
f"{row['apply_mdof_per_second_mad']:.2f} | "
|
||||
f"{speedup_text} | {solve_text} | {iteration_text} |"
|
||||
)
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def collect_metadata(args: argparse.Namespace) -> dict[str, Any]:
|
||||
executable = pathlib.Path(args.executable).resolve()
|
||||
source_root = pathlib.Path(__file__).resolve().parents[1]
|
||||
return {
|
||||
"timestamp_utc": dt.datetime.now(dt.timezone.utc).isoformat(),
|
||||
"hostname": platform.node(),
|
||||
"platform": platform.platform(),
|
||||
"python": sys.version,
|
||||
"cpu_count": os.cpu_count(),
|
||||
"lscpu": command_output(["lscpu"]) if shutil.which("lscpu") else None,
|
||||
"executable": str(executable),
|
||||
"launcher": str(pathlib.Path(args.launcher).resolve()),
|
||||
"launcher_version": command_output([args.launcher, "--version"]),
|
||||
"compiler": command_output(["c++", "--version"]),
|
||||
"nvidia_smi": command_output(
|
||||
[
|
||||
"nvidia-smi",
|
||||
"--query-gpu=name,driver_version,temperature.gpu,pstate,clocks.sm,power.draw",
|
||||
"--format=csv,noheader",
|
||||
]
|
||||
)
|
||||
if shutil.which("nvidia-smi")
|
||||
else None,
|
||||
"git_commit": command_output(
|
||||
["git", "-C", str(source_root), "rev-parse", "HEAD"]
|
||||
),
|
||||
"git_dirty": bool(
|
||||
command_output(["git", "-C", str(source_root), "status", "--porcelain"])
|
||||
),
|
||||
"parameters": {
|
||||
"mesh_n": args.mesh_n,
|
||||
"order": args.order,
|
||||
"applications": args.applications,
|
||||
"max_applications": args.max_applications,
|
||||
"minimum_apply_seconds": args.minimum_apply_seconds,
|
||||
"warmup_applications": args.warmup_applications,
|
||||
"trials": args.trials,
|
||||
"discarded_warmup_trials": args.warmup_trials,
|
||||
"relative_tolerance": args.relative_tolerance,
|
||||
"max_iterations": args.max_iterations,
|
||||
"solve": args.solve,
|
||||
"affinity": args.affinity,
|
||||
"network_interface": args.network_interface,
|
||||
"cuda_visible_devices": args.cuda_visible_devices,
|
||||
"gpu_aware_mpi": args.gpu_aware_mpi,
|
||||
"cross_backend_relative_tolerance": args.cross_backend_relative_tolerance,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def write_results(
|
||||
output_dir: pathlib.Path,
|
||||
metadata: dict[str, Any],
|
||||
records: list[dict[str, Any]],
|
||||
summaries: list[dict[str, Any]],
|
||||
) -> None:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
(output_dir / "results.json").write_text(
|
||||
json.dumps(
|
||||
{"schema_version": 1, "metadata": metadata, "raw": records, "summary": summaries},
|
||||
indent=2,
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
fieldnames = [
|
||||
"case",
|
||||
"device",
|
||||
"ranks",
|
||||
"threads",
|
||||
"trials",
|
||||
"global_true_dofs",
|
||||
"requested_applications",
|
||||
"applications_min",
|
||||
"applications_max",
|
||||
"apply_mdof_per_second_median",
|
||||
"apply_mdof_per_second_mad",
|
||||
"apply_mdof_per_second_min",
|
||||
"apply_mdof_per_second_max",
|
||||
"speedup_vs_cpu",
|
||||
"apply_seconds_median",
|
||||
"solve_seconds_median",
|
||||
"solve_ran",
|
||||
"total_seconds_median",
|
||||
"cg_iterations",
|
||||
"verified_relative_residual_max",
|
||||
]
|
||||
with (output_dir / "results.csv").open("w", newline="", encoding="utf-8") as handle:
|
||||
writer = csv.DictWriter(handle, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
for summary in summaries:
|
||||
writer.writerow(summary)
|
||||
first = records[0]
|
||||
if first["minimum_apply_seconds"] > 0.0:
|
||||
timing_description = (
|
||||
"Application counts were selected with a "
|
||||
f"{first['minimum_apply_seconds']:.3g}-second calibration window, then "
|
||||
"measured in a fresh timing window."
|
||||
)
|
||||
else:
|
||||
timing_description = (
|
||||
"The requested application count was measured directly in one timing "
|
||||
"window."
|
||||
)
|
||||
report = "# MFEM backend benchmark\n\n"
|
||||
report += (
|
||||
f"Host: `{metadata['hostname']}` \n"
|
||||
f"UTC timestamp: `{metadata['timestamp_utc']}` \n"
|
||||
f"Workload: {first['global_elements']:,} 3D hex elements, H1 order "
|
||||
f"{first['order']}, {first['global_true_dofs']:,} global true DoFs, "
|
||||
f"at least {first['requested_applications']} timed operator applications "
|
||||
f"per trial. {timing_description}\n\n"
|
||||
)
|
||||
report += markdown_table(summaries)
|
||||
report += (
|
||||
"\nThe table reports medians from fresh processes. Operator throughput uses "
|
||||
"the same global partial-assembly diffusion operator and true-DOF field in "
|
||||
"every case. MPI timings use the slowest rank.\n"
|
||||
)
|
||||
(output_dir / "results.md").write_text(report, encoding="utf-8")
|
||||
|
||||
|
||||
def parse_arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--executable", required=True)
|
||||
parser.add_argument("--launcher", required=True)
|
||||
parser.add_argument(
|
||||
"--cases",
|
||||
default="cpu,ceed-cpu,omp,mpi,cuda,ceed-cuda",
|
||||
help="Comma-separated subset of cpu, ceed-cpu, omp, mpi, cuda, ceed-cuda.",
|
||||
)
|
||||
parser.add_argument("--omp-threads", type=integer_list, default=[8])
|
||||
parser.add_argument("--mpi-ranks", type=integer_list, default=[4])
|
||||
parser.add_argument("--trials", type=positive_int, default=5)
|
||||
parser.add_argument("--warmup-trials", type=int, default=0)
|
||||
parser.add_argument("--mesh-n", type=positive_int, default=48)
|
||||
parser.add_argument("--order", type=positive_int, default=3)
|
||||
parser.add_argument("--applications", type=positive_int, default=50)
|
||||
parser.add_argument("--max-applications", type=positive_int, default=1_000_000)
|
||||
parser.add_argument(
|
||||
"--minimum-apply-seconds",
|
||||
type=float,
|
||||
default=1.0,
|
||||
help=(
|
||||
"Double the requested applications until a calibration window is this "
|
||||
"long, then measure a fresh window."
|
||||
),
|
||||
)
|
||||
parser.add_argument("--warmup-applications", type=int, default=5)
|
||||
parser.add_argument("--relative-tolerance", type=float, default=1.0e-6)
|
||||
parser.add_argument("--max-iterations", type=positive_int, default=1000)
|
||||
parser.add_argument(
|
||||
"--solve",
|
||||
action="store_true",
|
||||
help="Also run and time the Jacobi-preconditioned CG validation solve.",
|
||||
)
|
||||
parser.add_argument("--timeout", type=positive_int, default=900)
|
||||
parser.add_argument(
|
||||
"--affinity",
|
||||
default="auto",
|
||||
help="CPU list for taskset, 'auto' for a compact set, or 'none'.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--network-interface",
|
||||
default="local",
|
||||
help="'local' for platform loopback, 'auto' for routing, or an interface name.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cuda-visible-devices",
|
||||
default=None,
|
||||
help="Optional CUDA_VISIBLE_DEVICES value for GPU cases.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--gpu-aware-mpi",
|
||||
action="store_true",
|
||||
help="Opt in only when the selected MPI implementation is CUDA-aware.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cross-backend-relative-tolerance",
|
||||
type=float,
|
||||
default=1.0e-5,
|
||||
help="Maximum relative difference from CPU for probe and solution norms.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ceed-cpu-device",
|
||||
default="ceed-cpu",
|
||||
help="Full libCEED CPU resource or portable ceed-cpu alias.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ceed-cuda-device",
|
||||
default="ceed-cuda",
|
||||
help="Full libCEED CUDA resource or portable ceed-cuda alias.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--launcher-arg",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Additional MPI launcher argument; repeat as needed.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
type=pathlib.Path,
|
||||
default=pathlib.Path("benchmark-results"),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
if args.warmup_trials < 0 or args.warmup_applications < 0:
|
||||
parser.error("warm-up counts may not be negative")
|
||||
if args.relative_tolerance <= 0.0 or args.cross_backend_relative_tolerance <= 0.0:
|
||||
parser.error("tolerances must be positive")
|
||||
if args.minimum_apply_seconds < 0.0:
|
||||
parser.error("minimum apply seconds may not be negative")
|
||||
if args.max_applications < args.applications:
|
||||
parser.error("max applications must be at least the requested applications")
|
||||
for attribute in ("executable", "launcher"):
|
||||
value = getattr(args, attribute)
|
||||
resolved = resolve_program(value)
|
||||
if resolved is None:
|
||||
parser.error(f"program not found: {value}")
|
||||
setattr(args, attribute, resolved)
|
||||
return args
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_arguments()
|
||||
try:
|
||||
cases = expand_cases(args)
|
||||
except ValueError as error:
|
||||
print(f"error: {error}", file=sys.stderr)
|
||||
return 2
|
||||
metadata = collect_metadata(args)
|
||||
records: list[dict[str, Any]] = []
|
||||
|
||||
for warmup in range(args.warmup_trials):
|
||||
for case in cases:
|
||||
print(f"warm-up {warmup + 1}/{args.warmup_trials}: {case.name}", flush=True)
|
||||
records.append(run_case(args, case, -(warmup + 1), True))
|
||||
checkpoint_results(args.output_dir, metadata, records)
|
||||
|
||||
schedule = [(trial, case) for trial in range(1, args.trials + 1) for case in cases]
|
||||
random.Random(20260905).shuffle(schedule)
|
||||
for index, (trial, case) in enumerate(schedule, start=1):
|
||||
print(
|
||||
f"run {index}/{len(schedule)}: {case.name}, trial {trial}/{args.trials}",
|
||||
flush=True,
|
||||
)
|
||||
records.append(run_case(args, case, trial, False))
|
||||
checkpoint_results(args.output_dir, metadata, records)
|
||||
|
||||
summaries = summarize(cases, records)
|
||||
workload_signatures = {
|
||||
(
|
||||
row["global_elements"],
|
||||
row["global_true_dofs"],
|
||||
row["mesh_n"],
|
||||
row["order"],
|
||||
row["requested_applications"],
|
||||
row["minimum_apply_seconds"],
|
||||
)
|
||||
for row in records
|
||||
if not row["discarded_warmup_trial"]
|
||||
}
|
||||
if len(workload_signatures) != 1:
|
||||
raise RuntimeError(
|
||||
f"cases did not use one fixed global workload: {workload_signatures}"
|
||||
)
|
||||
metadata["cross_backend_maximum_relative_errors"] = validate_cross_backend(
|
||||
records, args.cross_backend_relative_tolerance
|
||||
)
|
||||
if shutil.which("nvidia-smi"):
|
||||
metadata["nvidia_smi_after"] = command_output(
|
||||
[
|
||||
"nvidia-smi",
|
||||
"--query-gpu=name,driver_version,temperature.gpu,pstate,clocks.sm,power.draw",
|
||||
"--format=csv,noheader",
|
||||
]
|
||||
)
|
||||
write_results(args.output_dir, metadata, records, summaries)
|
||||
table = markdown_table(summaries)
|
||||
print("\n" + table)
|
||||
print(f"JSON, CSV, and Markdown written to {args.output_dir.resolve()}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except (OSError, subprocess.TimeoutExpired, RuntimeError) as error:
|
||||
print(f"benchmark failed: {error}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
Reference in New Issue
Block a user