Skip to content

Commit fa71e81

Browse files
committed
Attend to clang-tidy comments
1 parent 7e2a1c2 commit fa71e81

4 files changed

Lines changed: 218 additions & 172 deletions

File tree

src/targets/gpu/compile_ops.cpp

Lines changed: 170 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
#include <migraphx/gpu/lower_device_ops.hpp>
4747
#include <migraphx/gpu/time_op.hpp>
4848
#include <algorithm>
49+
#include <cassert>
4950
#include <cmath>
5051
#include <cstdlib>
5152
#include <functional>
@@ -104,6 +105,30 @@ static std::size_t compute_benchmark_bundle(const module& m)
104105
return std::max(1, 4 * n - 2);
105106
}
106107

108+
// Executions one candidate may still spend. The lifetime total matches the fixed per-candidate
109+
// work of the pre-adaptive schedule and is drawn down by both timing stages.
110+
struct candidate_budget
111+
{
112+
std::size_t remaining = 0;
113+
std::size_t stage_limit = 0;
114+
115+
bool exhausted() const { return remaining == 0; }
116+
};
117+
118+
static candidate_budget
119+
make_candidate_budget(adaptive_time_stage stage, std::size_t bundle, std::size_t used)
120+
{
121+
const auto lifetime = 1 + benchmark_samples * bundle;
122+
if(used >= lifetime)
123+
return {};
124+
const auto remaining = lifetime - used;
125+
if(stage == adaptive_time_stage::precise)
126+
return {remaining, remaining};
127+
// Coarse timing only needs lazy initialization, one estimate, and up to coarse_samples bundled
128+
// ranking measurements, so it leaves most of the lifetime budget to the finalists.
129+
return {remaining, std::min(remaining, 2 + coarse_samples * bundle)};
130+
}
131+
107132
struct precompile_op
108133
{
109134
operation op = op::identity{};
@@ -349,6 +374,36 @@ struct shared_benchmark_inputs
349374
}
350375
};
351376

377+
// State that has to survive from one benchmarked candidate to the next.
378+
struct benchmark_state
379+
{
380+
explicit benchmark_state(std::size_t candidate_count) : execution_counts(candidate_count) {}
381+
382+
std::vector<shared_benchmark_inputs> shared_inputs;
383+
// How much of each candidate's lifetime execution budget the timing stages have spent.
384+
std::vector<std::size_t> execution_counts;
385+
// GPU failures tend to be sticky, so one broken candidate usually takes every later one down
386+
// with it. Keep the first message to report instead of a more general message.
387+
std::string first_error;
388+
389+
std::shared_ptr<parameter_map>
390+
find_reusable(const std::unordered_map<std::string, double>& fill_map,
391+
const std::unordered_map<std::string, shape>& parameter_shapes) const
392+
{
393+
auto it = std::find_if(shared_inputs.begin(), shared_inputs.end(), [&](const auto& x) {
394+
return x.matches(fill_map, parameter_shapes);
395+
});
396+
return it == shared_inputs.end() ? nullptr : it->params;
397+
}
398+
399+
void keep_for_reuse(const std::unordered_map<std::string, double>& fill_map,
400+
std::unordered_map<std::string, shape> parameter_shapes,
401+
std::shared_ptr<parameter_map> params)
402+
{
403+
shared_inputs.push_back({fill_map, std::move(parameter_shapes), std::move(params)});
404+
}
405+
};
406+
352407
// forward declared since it requires compile_manager
353408
static void replace_inserted_device_ops(context& ctx, module& m);
354409

@@ -469,6 +524,110 @@ struct compile_plan
469524
}
470525
return input_shapes.str();
471526
}
527+
std::string no_valid_compilation_message() const
528+
{
529+
return "No valid tuned compilation for " + preop.name() + " with " + problem_string() +
530+
"\n\n" + print_modules();
531+
}
532+
std::string no_valid_benchmark_message(const std::string& first_error) const
533+
{
534+
return "No valid tuned benchmark for " + preop.name() + " with " + problem_string() +
535+
(first_error.empty() ? "" : "\n\nFirst error: " + first_error) + "\n\n" +
536+
print_modules();
537+
}
538+
539+
// Builds the runnable form of one candidate, reusing input buffers from an earlier candidate
540+
// when both the fill policy and the parameter layout agree.
541+
prepared_time_program prepare_candidate(benchmark_state& state,
542+
std::size_t i,
543+
adaptive_time_stage stage,
544+
program bench_prog) const
545+
{
546+
// Finalists are measured from fresh buffers, so release what the coarse stage was sharing
547+
// before this candidate allocates its own.
548+
if(stage == adaptive_time_stage::precise)
549+
state.shared_inputs.clear();
550+
551+
const auto& fill_map = results[i]->replace.fill_map;
552+
auto parameter_shapes = bench_prog.get_parameter_shapes();
553+
auto reusable = state.find_reusable(fill_map, parameter_shapes);
554+
555+
auto prepared = prepare_time_program(*ctx, std::move(bench_prog), fill_map, reusable);
556+
// find_reusable already matched the parameter layout, so the hint is never rejected.
557+
assert(reusable == nullptr or prepared.params == reusable);
558+
if(stage == adaptive_time_stage::coarse and reusable == nullptr)
559+
state.keep_for_reuse(fill_map, std::move(parameter_shapes), prepared.params);
560+
return prepared;
561+
}
562+
563+
// Times a single candidate, as required by adaptive_time_topk_staged. A candidate that cannot
564+
// be timed is reported as unmeasured rather than as an error so that tuning can continue.
565+
optional<double> time_solution(benchmark_state& state,
566+
std::size_t i,
567+
adaptive_time_stage stage,
568+
const adaptive_time_options& input_options) const
569+
{
570+
const auto trace_level = value_of(MIGRAPHX_TRACE_BENCHMARKING{});
571+
if(not results[i].has_value())
572+
{
573+
if(trace_level > 1)
574+
std::cout << "No binary for solution: " << config->solutions.at(i) << std::endl;
575+
return nullopt;
576+
}
577+
578+
if(trace_level > 1)
579+
std::cout << (stage == adaptive_time_stage::coarse ? "Coarsely" : "Precisely")
580+
<< " benchmarking solution: " << config->solutions.at(i) << std::endl;
581+
// Held for one timing only so that loaded code objects and input buffers are not
582+
// retained for every candidate at once.
583+
optional<prepared_time_program> prepared;
584+
try
585+
{
586+
if(trace_level > 2)
587+
std::cout << *results[i] << std::endl;
588+
/*
589+
* Replacing the instruction in this small program inserts every code object
590+
* and prefill required by the candidate, so split-k is timed end to end.
591+
*/
592+
auto bench_prog = results[i]->make_program();
593+
if(trace_level > 2)
594+
std::cout << bench_prog << std::endl;
595+
const auto bundle = compute_benchmark_bundle(*bench_prog.get_main_module());
596+
const auto budget = make_candidate_budget(stage, bundle, state.execution_counts[i]);
597+
if(budget.exhausted())
598+
return nullopt;
599+
600+
prepared = prepare_candidate(state, i, stage, std::move(bench_prog));
601+
if(trace_level > 1)
602+
std::cout << "Prepared benchmark solution: " << config->solutions.at(i)
603+
<< std::endl;
604+
605+
auto options = input_options;
606+
options.preferred_bundle = bundle;
607+
options.max_executions = std::min(options.max_executions, budget.remaining);
608+
adaptive_time_budget stage_budget;
609+
stage_budget.max_executions = budget.stage_limit;
610+
auto measured = adaptive_time_program(*prepared, options, stage_budget);
611+
state.execution_counts[i] += prepared->executions;
612+
if(not std::isfinite(measured) or measured <= 0.0)
613+
return nullopt;
614+
if(trace_level > 1)
615+
std::cout << measured << "ms" << std::endl;
616+
return measured;
617+
}
618+
catch(const std::exception& e)
619+
{
620+
if(prepared.has_value())
621+
state.execution_counts[i] += prepared->executions;
622+
if(state.first_error.empty())
623+
state.first_error =
624+
"solution " + to_string(config->solutions.at(i)) + ": " + e.what();
625+
if(trace_level > 0)
626+
std::cerr << "Exception benchmarking " << preop.name() << " solution "
627+
<< config->solutions.at(i) << ": " << e.what() << std::endl;
628+
return nullopt;
629+
}
630+
}
472631

473632
const compiled_result& benchmark() const
474633
{
@@ -479,13 +638,11 @@ struct compile_plan
479638
<< std::endl;
480639
}
481640
if(results.empty())
482-
MIGRAPHX_THROW("No valid tuned compilation for " + preop.name() + " with " +
483-
problem_string() + "\n\n" + print_modules());
641+
MIGRAPHX_THROW(no_valid_compilation_message());
484642
if(results.size() == 1)
485643
{
486644
if(not results.front().has_value())
487-
MIGRAPHX_THROW("No valid tuned compilation for " + preop.name() + " with " +
488-
problem_string() + "\n\n" + print_modules());
645+
MIGRAPHX_THROW(no_valid_compilation_message());
489646
return *results.front();
490647
}
491648
if(not config)
@@ -500,8 +657,7 @@ struct compile_plan
500657
std::back_inserter(valid_indices),
501658
[&](auto i) { return results[i].has_value(); });
502659
if(valid_indices.empty())
503-
MIGRAPHX_THROW("No valid tuned compilation for " + preop.name() + " with " +
504-
problem_string() + "\n\n" + print_modules());
660+
MIGRAPHX_THROW(no_valid_compilation_message());
505661

506662
if(valid_indices.size() == 1)
507663
{
@@ -511,119 +667,17 @@ struct compile_plan
511667
}
512668

513669
auto tuning_options = tuning;
670+
// Every valid candidate would reach precise timing anyway, so skip the ranking stage.
514671
if(tuning_options.top_k >= valid_indices.size())
515672
tuning_options.top_k = 0;
516673

517-
// GPU failures tend to be sticky, so one broken candidate usually takes every later one
518-
// down with it. Keep the first message to report instead of a more general message.
519-
std::string first_error;
520-
std::vector<std::size_t> execution_counts(results.size());
521-
// Coarse candidates share input buffers when their fill policy and parameter layout agree.
522-
// The buffers are released before precise timing so finalists are measured from fresh
523-
// state.
524-
std::vector<shared_benchmark_inputs> shared_inputs;
525-
auto time_solution = [&](std::size_t i,
526-
adaptive_time_stage stage,
527-
const adaptive_time_options& input_options) -> optional<double> {
528-
if(not results[i].has_value())
529-
{
530-
if(trace_level > 1)
531-
std::cout << "No binary for solution: " << config->solutions.at(i) << std::endl;
532-
return nullopt;
533-
}
534-
535-
if(trace_level > 1)
536-
std::cout << (stage == adaptive_time_stage::coarse ? "Coarsely" : "Precisely")
537-
<< " benchmarking solution: " << config->solutions.at(i) << std::endl;
538-
// Held for one timing only so that loaded code objects and input buffers are not
539-
// retained for every candidate at once.
540-
optional<prepared_time_program> prepared;
541-
try
542-
{
543-
if(trace_level > 2)
544-
std::cout << *results[i] << std::endl;
545-
// Precise candidates start from fresh programs and parameters, while coarse
546-
// executions remain charged to each candidate's lifetime execution budget.
547-
if(stage == adaptive_time_stage::precise)
548-
shared_inputs.clear();
549-
/*
550-
* Replacing the instruction in this small program inserts every code object
551-
* and prefill required by the candidate, so split-k is timed end to end.
552-
*/
553-
auto bench_prog = results[i]->make_program();
554-
if(trace_level > 2)
555-
std::cout << bench_prog << std::endl;
556-
const auto bundle = compute_benchmark_bundle(*bench_prog.get_main_module());
557-
558-
// Lifetime budget for this candidate, drawn down by both timing stages.
559-
const auto candidate_budget = 1 + benchmark_samples * bundle;
560-
const auto used = execution_counts[i];
561-
if(used >= candidate_budget)
562-
return nullopt;
563-
const auto remaining = candidate_budget - used;
564-
565-
const auto& fill_map = results[i]->replace.fill_map;
566-
auto parameter_shapes = bench_prog.get_parameter_shapes();
567-
auto shared = shared_inputs.end();
568-
if(stage == adaptive_time_stage::coarse)
569-
shared = std::find_if(
570-
shared_inputs.begin(), shared_inputs.end(), [&](const auto& x) {
571-
return x.matches(fill_map, parameter_shapes);
572-
});
573-
prepared =
574-
prepare_time_program(*ctx,
575-
std::move(bench_prog),
576-
fill_map,
577-
shared == shared_inputs.end() ? nullptr : shared->params);
578-
if(stage == adaptive_time_stage::coarse and shared == shared_inputs.end())
579-
shared_inputs.push_back(
580-
{fill_map, std::move(parameter_shapes), prepared->params});
581-
if(trace_level > 1)
582-
std::cout << "Prepared benchmark solution: " << config->solutions.at(i)
583-
<< std::endl;
584-
585-
auto options = input_options;
586-
options.preferred_bundle = bundle;
587-
options.max_executions = std::min(options.max_executions, remaining);
588-
adaptive_time_budget budget;
589-
budget.max_executions = remaining;
590-
if(stage == adaptive_time_stage::coarse)
591-
{
592-
// Coarse timing needs lazy initialization, one estimate, and up to
593-
// coarse_samples bundled ranking measurements. Precise timing receives the
594-
// lifetime budget left by this call.
595-
budget.max_executions =
596-
std::min(budget.max_executions, 2 + coarse_samples * bundle);
597-
}
598-
auto measured = adaptive_time_program(*prepared, options, budget);
599-
execution_counts[i] += prepared->executions;
600-
if(not std::isfinite(measured) or measured <= 0.0)
601-
return nullopt;
602-
if(trace_level > 1)
603-
std::cout << measured << "ms" << std::endl;
604-
return measured;
605-
}
606-
catch(const std::exception& e)
607-
{
608-
if(prepared.has_value())
609-
execution_counts[i] += prepared->executions;
610-
if(first_error.empty())
611-
first_error =
612-
"solution " + to_string(config->solutions.at(i)) + ": " + e.what();
613-
if(trace_level > 0)
614-
std::cerr << "Exception benchmarking " << preop.name() << " solution "
615-
<< config->solutions.at(i) << ": " << e.what() << std::endl;
616-
return nullopt;
617-
}
618-
};
619-
620-
const auto winner =
621-
adaptive_time_topk_staged(results.size(), tuning_options, time_solution);
674+
benchmark_state state{results.size()};
675+
const auto winner = adaptive_time_topk_staged(
676+
results.size(), tuning_options, [&](auto i, auto stage, const auto& options) {
677+
return time_solution(state, i, stage, options);
678+
});
622679
if(not winner.has_value())
623-
MIGRAPHX_THROW("No valid tuned benchmark for " + preop.name() + " with " +
624-
problem_string() +
625-
(first_error.empty() ? "" : "\n\nFirst error: " + first_error) + "\n\n" +
626-
print_modules());
680+
MIGRAPHX_THROW(no_valid_benchmark_message(state.first_error));
627681
const auto i = *winner;
628682
ctx->get_problem_cache().insert(preop.name(), config->problem, config->solutions.at(i));
629683
if(trace_level > 0)
@@ -632,8 +686,7 @@ struct compile_plan
632686
ctx->get_problem_cache().save();
633687
}
634688
if(not results[i].has_value())
635-
MIGRAPHX_THROW("No valid tuned compilation for " + preop.name() + " with " +
636-
problem_string() + "\n\n" + print_modules());
689+
MIGRAPHX_THROW(no_valid_compilation_message());
637690
auto skipped = std::count_if(
638691
results.begin(), results.end(), [](const auto& cr) { return not cr.has_value(); });
639692
if(skipped > 0)
@@ -816,4 +869,3 @@ void compile_ops::apply(module_pass_manager& mpm) const
816869

817870
} // namespace MIGRAPHX_INLINE_NS
818871
} // namespace migraphx
819-

0 commit comments

Comments
 (0)