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+
107132struct 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
353408static 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\n First 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\n First 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