CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
217 lines
11 KiB
C++
217 lines
11 KiB
C++
// Copyright Advanced Micro Devices, Inc., or its affiliates.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#pragma once
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <limits>
|
|
#include <sstream>
|
|
#include <string>
|
|
|
|
// Dependency-free config/result types (and their validation) for the adaptive
|
|
// timing routine in benchmark_timing.hpp, separated so argument_model.hpp can
|
|
// use TimingResult without including the timing implementation's HIP headers.
|
|
|
|
namespace hipblaslt_bench
|
|
{
|
|
// All time budgets are in milliseconds.
|
|
struct TimingConfig
|
|
{
|
|
bool adaptive = false; // false => fixed-count fast path (uses iters); true => adaptive
|
|
int32_t iters = 10; // enqueues in the fixed-count sample; unused by the adaptive path
|
|
float warmup_time = 0.0f; // warm up until this wall-time is reached
|
|
float sample_time = 0.0f; // target span per sample; 0 => one enqueue per sample
|
|
float measure_time = 0.0f; // minimum total measure time (floor)
|
|
float max_measure_time = 0.0f; // measure ceiling; 0 => unbounded
|
|
int32_t min_iters = 0; // floor on total timed iterations (0 in the fixed-count fast path)
|
|
int32_t max_iters = 0; // ceiling on total timed iterations; 0 => unbounded
|
|
float noise_threshold = 0.0f; // rel. std error convergence target; 0 => disabled
|
|
// Noise-plateau fallback: past the floor, end the run if convergence cannot be
|
|
// reached but the robust spread (rel_iqr) has settled. 0 threshold disables it.
|
|
float stability_threshold = 0.0f; // max rel. spread of recent rel_iqr readings to stop
|
|
int32_t stability_window = 0; // rel_iqr readings tested for the plateau (>= 2)
|
|
int32_t stability_interval = 0; // record a rel_iqr reading every N samples (>= 1)
|
|
bool use_gpu_timer = false; // hipEvent timing vs CPU wall clock
|
|
double flush_us = 0.0; // per-launch flush_icache overhead, subtracted/sample
|
|
};
|
|
|
|
// Validate the semantic constraints of an adaptive TimingConfig. Returns an empty
|
|
// string on success, or a human-readable error description on failure. All entry
|
|
// points (CLI, YAML, run_measurement) call this as their single source of truth.
|
|
inline std::string validate_adaptive_config(const TimingConfig& cfg)
|
|
{
|
|
// Reject negative inputs up front: a negative time floor (measure_time) would be
|
|
// instantly satisfied and a negative warmup_time would skip warmup, both yielding
|
|
// subtly wrong measurements; negative thresholds would silently read as "disabled".
|
|
if(cfg.warmup_time < 0.0f)
|
|
return "warmup_time (" + std::to_string(cfg.warmup_time) + " ms) must be >= 0";
|
|
if(cfg.sample_time < 0.0f)
|
|
return "sample_time (" + std::to_string(cfg.sample_time) + " ms) must be >= 0";
|
|
if(cfg.measure_time < 0.0f)
|
|
return "measure_time (" + std::to_string(cfg.measure_time) + " ms) must be >= 0";
|
|
if(cfg.max_measure_time < 0.0f)
|
|
return "max_measure_time (" + std::to_string(cfg.max_measure_time)
|
|
+ " ms) must be >= 0";
|
|
if(cfg.min_iters < 0)
|
|
return "min_iters (" + std::to_string(cfg.min_iters) + ") must be >= 0";
|
|
if(cfg.max_iters < 0)
|
|
return "max_iters (" + std::to_string(cfg.max_iters) + ") must be >= 0";
|
|
if(cfg.noise_threshold < 0.0f)
|
|
return "noise_threshold (" + std::to_string(cfg.noise_threshold) + ") must be >= 0";
|
|
if(cfg.stability_threshold < 0.0f)
|
|
return "stability_threshold (" + std::to_string(cfg.stability_threshold)
|
|
+ ") must be >= 0";
|
|
|
|
if(cfg.max_iters > 0 && cfg.min_iters > cfg.max_iters)
|
|
return "min_iters (" + std::to_string(cfg.min_iters) + ") > max_iters ("
|
|
+ std::to_string(cfg.max_iters) + ")";
|
|
if(cfg.max_measure_time > 0.0f && cfg.measure_time > cfg.max_measure_time)
|
|
return "measure_time (" + std::to_string(cfg.measure_time)
|
|
+ " ms) > max_measure_time (" + std::to_string(cfg.max_measure_time) + " ms)";
|
|
if(cfg.max_measure_time <= 0.0f && cfg.max_iters <= 0)
|
|
return "adaptive timing requires a ceiling: set max_measure_time or max_iters > 0";
|
|
if(cfg.stability_threshold > 0.0f
|
|
&& (cfg.stability_window < 2 || cfg.stability_interval < 1))
|
|
return "stability_threshold > 0 requires stability_window >= 2 and "
|
|
"stability_interval >= 1";
|
|
return {};
|
|
}
|
|
|
|
// Default values for the --adaptive preset, used as the CLI/YAML defaults so they
|
|
// appear directly in --help. Keep hipblaslt_common.yaml's Defaults block in sync.
|
|
namespace adaptive_defaults
|
|
{
|
|
constexpr float warmup_time = 50.0f; // ms warmup budget
|
|
constexpr float sample_time = 1.0f; // ms span per timed sample
|
|
constexpr float measure_time = 500.0f; // ms measurement floor
|
|
constexpr float max_measure_time = 2000.0f; // ms ceiling for convergence
|
|
constexpr int32_t min_iters = 10; // floor on total timed iterations
|
|
constexpr int32_t max_iters = 0; // unbounded
|
|
constexpr float noise_threshold = 0.01f; // 1% relative standard error
|
|
// Noise-plateau fallback (see TimingConfig). window * interval = 512-sample look-back,
|
|
// so the fallback can only fire ~512 samples past the measure_time floor: kernels that
|
|
// converge first report "converged", only those that cannot report "stable".
|
|
constexpr float stability_threshold = 0.05f; // 5% rel. spread over the window
|
|
constexpr int32_t stability_window = 32; // readings tested (512-sample look-back)
|
|
constexpr int32_t stability_interval = 16; // a reading every 16 samples
|
|
}
|
|
|
|
// Per-iteration statistics, in microseconds.
|
|
struct TimingResult
|
|
{
|
|
double median_us = 0.0;
|
|
double min_us = 0.0;
|
|
double mean_us = 0.0;
|
|
double cv = 0.0; // coefficient of variation across samples (stddev/mean, n-1)
|
|
double rel_iqr = 0.0; // robust dispersion: interquartile range / median
|
|
int32_t batch = 0; // enqueues per sample (B)
|
|
int32_t samples = 0; // number of samples collected (K)
|
|
int64_t hot_iters = 0; // total timed enqueues (B*K)
|
|
bool adaptive = false; // the adaptive path ran (vs the fixed-count fast path)
|
|
bool noise_active = false; // convergence checking was enabled (noise_threshold > 0)
|
|
bool converged = false; // precision target met (meaningful only when noise_active)
|
|
bool stable = false; // robust-dispersion (rel_iqr) plateau reached: the
|
|
// distribution is characterized though the precision target was not met (fallback exit)
|
|
};
|
|
|
|
// Result of sizing the rotating buffer's block count (see compute_rotating_buffer_plan).
|
|
struct RotatingBufferPlan
|
|
{
|
|
int32_t block_count = 1; // number of physical buffer copies to allocate
|
|
int32_t iter_cap = 0; // iteration bound considered; meaningful only when capped
|
|
bool capped = false; // true if iter_cap was the binding constraint
|
|
};
|
|
|
|
// Sizes the rotating buffer's block count: enough blocks to cover rotating_bytes, but
|
|
// never more than the total iterations that will run (blocks are indexed via
|
|
// i % block_count, so extra blocks past the iteration count go unused). Adaptive mode
|
|
// ignores cold_iters/iters and caps on adaptive_max_iters instead; 0 there is unbounded.
|
|
inline RotatingBufferPlan compute_rotating_buffer_plan(bool adaptive,
|
|
int32_t adaptive_max_iters,
|
|
int32_t cold_iters,
|
|
int32_t iters,
|
|
int64_t rotating_bytes,
|
|
int64_t total_size_needed_bytes)
|
|
{
|
|
RotatingBufferPlan plan;
|
|
plan.iter_cap = adaptive ? adaptive_max_iters : std::max(cold_iters, iters);
|
|
bool apply_cap = !(adaptive && plan.iter_cap == 0);
|
|
|
|
// A zero-byte-per-iteration problem has nothing to rotate
|
|
if(total_size_needed_bytes <= 0)
|
|
{
|
|
plan.block_count = 1;
|
|
plan.capped = false;
|
|
return plan;
|
|
}
|
|
|
|
double mem_blocks = std::ceil(static_cast<double>(rotating_bytes)
|
|
/ static_cast<double>(total_size_needed_bytes));
|
|
|
|
// Edge case: mem_blocks can exceed int32_t's range for a huge budget vs. a tiny
|
|
// per-iteration size, and casting an out-of-range double is undefined behavior.
|
|
// When that happens the memory side has no usable bound: fall back to the
|
|
// iteration cap, or to a single block when there isn't one either.
|
|
constexpr double max_blocks = static_cast<double>(std::numeric_limits<int32_t>::max());
|
|
if(mem_blocks > max_blocks)
|
|
{
|
|
plan.block_count = apply_cap ? std::max(1, plan.iter_cap) : 1;
|
|
plan.capped = apply_cap;
|
|
return plan;
|
|
}
|
|
|
|
// Both sides are now valid, in-range values; take the smaller when a cap applies.
|
|
double chosen
|
|
= apply_cap ? std::min(static_cast<double>(plan.iter_cap), mem_blocks) : mem_blocks;
|
|
plan.block_count = std::max(1, static_cast<int32_t>(chosen));
|
|
plan.capped = apply_cap && static_cast<double>(plan.iter_cap) < mem_blocks;
|
|
return plan;
|
|
}
|
|
|
|
// Formats a one-time, human-readable summary of a resolved adaptive-timing configuration,
|
|
// handling each field's "0 => unbounded/disabled" special case (see TimingConfig above).
|
|
// No trailing newline.
|
|
inline std::string format_adaptive_timing_summary(const TimingConfig& cfg)
|
|
{
|
|
std::ostringstream oss;
|
|
oss << "Adaptive timing: warmup " << cfg.warmup_time << "ms. Sample ";
|
|
if(cfg.sample_time == 0.0f)
|
|
oss << "single-enqueue";
|
|
else
|
|
oss << cfg.sample_time << "ms";
|
|
oss << ". Measure " << cfg.measure_time << "ms (max ";
|
|
if(cfg.max_measure_time == 0.0f)
|
|
oss << "unbounded";
|
|
else
|
|
oss << cfg.max_measure_time << "ms";
|
|
oss << "). Iters " << cfg.min_iters << " (max ";
|
|
if(cfg.max_iters == 0)
|
|
oss << "unbounded";
|
|
else
|
|
oss << cfg.max_iters;
|
|
oss << "). Noise threshold ";
|
|
if(cfg.noise_threshold == 0.0f)
|
|
oss << "disabled";
|
|
else
|
|
oss << cfg.noise_threshold * 100 << "%";
|
|
if(cfg.stability_threshold == 0.0f)
|
|
oss << " (stability fallback disabled)";
|
|
else
|
|
oss << " (stability " << cfg.stability_threshold * 100 << "%, window "
|
|
<< cfg.stability_window << ", interval " << cfg.stability_interval << ")";
|
|
oss << ".";
|
|
return oss.str();
|
|
}
|
|
|
|
// Converts a per-call amount to a per-second rate (amount / time_us * 1e6). A
|
|
// flush-overhead-corrected time_us can land at or below zero (e.g. a near-instant
|
|
// kernel), which would otherwise divide into inf/-inf/nan; returns invalid_value
|
|
// instead.
|
|
inline double rate_per_second(double amount, double time_us, double invalid_value)
|
|
{
|
|
return time_us > 0.0 ? amount / time_us * 1e6 : invalid_value;
|
|
}
|
|
} // namespace hipblaslt_bench
|