已合并
feat: 支持自动多流配置与自动寻优工作流 #4301
KenChow创建于 23 天前
feat: 支持自动多流配置与自动寻优工作流 #4301
已合并
共 42 个文件变更+3619-91
| @@ -1224,6 +1224,14 @@ Status ModelBuilder::BuildModelDefForStream(ge::Model &model) { | |||
| 1224 | ATTR_MODEL_EVENT_NUM.c_str()); | 1224 | ATTR_MODEL_EVENT_NUM.c_str()); |
| 1225 | GE_ASSERT_TRUE(ge::AttrUtils::SetListInt(&model, ATTR_MODEL_HUGE_STREAM_LIST, huge_streams_), | 1225 | GE_ASSERT_TRUE(ge::AttrUtils::SetListInt(&model, ATTR_MODEL_HUGE_STREAM_LIST, huge_streams_), |
| 1226 | "[Set][Attr] %s in model failed", ATTR_MODEL_HUGE_STREAM_LIST.c_str()); | 1226 | "[Set][Attr] %s in model failed", ATTR_MODEL_HUGE_STREAM_LIST.c_str()); |
| 1227 | + const auto root_graph = GraphUtils::FindRootGraph(compute_graph_); | ||
| 1228 | + GE_ASSERT_NOTNULL(root_graph); | ||
| 1229 | + std::string tuning_mode; | ||
| 1230 | + if (ge::AttrUtils::GetStr(root_graph, ATTR_MODEL_AUTO_MULTISTREAM_TUNING_MODE, tuning_mode) && | ||
| 1231 | + (!tuning_mode.empty())) { | ||
| 1232 | + GE_ASSERT_TRUE(ge::AttrUtils::SetStr(&model, ATTR_MODEL_AUTO_MULTISTREAM_TUNING_MODE, tuning_mode), | ||
| 1233 | + "[Set][Attr] %s in model failed", ATTR_MODEL_AUTO_MULTISTREAM_TUNING_MODE.c_str()); | ||
| 1234 | + } | ||
| 1227 | const auto graph = model.GetGraph(); | 1235 | const auto graph = model.GetGraph(); |
| 1228 | GE_ASSERT_NOTNULL(graph); | 1236 | GE_ASSERT_NOTNULL(graph); |
| 1229 | GE_ASSERT_TRUE(ge::AttrUtils::SetStr(graph, "_split_logic_stream_2_origin_logic_stream", | 1237 | GE_ASSERT_TRUE(ge::AttrUtils::SetStr(graph, "_split_logic_stream_2_origin_logic_stream", |
| @@ -12,9 +12,9 @@ | |||
| 12 | 12 | ||
| 13 | 13 | ||
| 14 | 14 | ||
| 15 | + | ||
| 15 | 16 | ||
| 16 | 17 | ||
| 17 | - | ||
| 18 | 18 | ||
| 19 | 19 | ||
| 20 | 20 | ||
| @@ -23,8 +23,6 @@ | |||
| 23 | 23 | ||
| 24 | namespace ge { | 24 | namespace ge { |
| 25 | namespace { | 25 | namespace { |
| 26 | -constexpr int32_t kMaxStreamLimit = 64; | ||
| 27 | - | ||
| 28 | const char_t *GetStrategyName(const minidag::StreamMergeStrategy strategy) { | 26 | const char_t *GetStrategyName(const minidag::StreamMergeStrategy strategy) { |
| 29 | switch (strategy) { | 27 | switch (strategy) { |
| 30 | case minidag::StreamMergeStrategy::kLoadBalance: | 28 | case minidag::StreamMergeStrategy::kLoadBalance: |
| @@ -38,62 +36,16 @@ const char_t *GetStrategyName(const minidag::StreamMergeStrategy strategy) { | |||
| 38 | } | 36 | } |
| 39 | } | 37 | } |
| 40 | 38 | ||
| 41 | -bool ParseStreamConfig(const std::string &multi_stream_mode, int64_t &out_max_stream_id, | 39 | +minidag::StreamMergeStrategy ToMiniDagStrategy(const AutoMultistreamMode mode) { |
| 42 | - minidag::StreamMergeStrategy &out_strategy) { | 40 | + switch (mode) { |
| 43 | - auto readable = GetContext().GetReadableName("ge.autoMultistreamParallelMode"); | 41 | + case AutoMultistreamMode::kMainStream: |
| 44 | - | 42 | + return minidag::StreamMergeStrategy::kMainStream; |
| 45 | - auto colon_pos = multi_stream_mode.find(':'); | 43 | + case AutoMultistreamMode::kWeightedLoadBalance: |
| 46 | - if (colon_pos == std::string::npos) { | 44 | + return minidag::StreamMergeStrategy::kWeightedLoadBalance; |
| 47 | - (void)REPORT_PREDEFINED_ERR_MSG("E10001", std::vector<const char_t *>({"value", "parameter", "reason"}), | 45 | + case AutoMultistreamMode::kLoadBalance: |
| 48 | - std::vector<const char_t *>({multi_stream_mode.c_str(), readable.c_str(), | 46 | + default: |
| 49 | - "Format error: missing colon separator."})); | 47 | + return minidag::StreamMergeStrategy::kLoadBalance; |
| 50 | - GELOGE(FAILED, "%s format error: missing colon separator, value=%s.", readable.c_str(), multi_stream_mode.c_str()); | ||
| 51 | - return false; | ||
| 52 | } | 48 | } |
| 53 | - | ||
| 54 | - std::string algo = multi_stream_mode.substr(0, colon_pos); | ||
| 55 | - if (algo.empty()) { | ||
| 56 | - (void)REPORT_PREDEFINED_ERR_MSG("E10001", std::vector<const char_t *>({"value", "parameter", "reason"}), | ||
| 57 | - std::vector<const char_t *>({multi_stream_mode.c_str(), readable.c_str(), | ||
| 58 | - "Format error: algo name is empty."})); | ||
| 59 | - GELOGE(FAILED, "%s format error: algo name is empty.", readable.c_str()); | ||
| 60 | - return false; | ||
| 61 | - } | ||
| 62 | - | ||
| 63 | - if (algo == "MainStream") { | ||
| 64 | - out_strategy = minidag::StreamMergeStrategy::kMainStream; | ||
| 65 | - } else if (algo == "LoadBalance") { | ||
| 66 | - out_strategy = minidag::StreamMergeStrategy::kLoadBalance; | ||
| 67 | - } else if (algo == "WeightedLoadBalance") { | ||
| 68 | - out_strategy = minidag::StreamMergeStrategy::kWeightedLoadBalance; | ||
| 69 | - } else { | ||
| 70 | - const auto invalid_strategy = static_cast<minidag::StreamMergeStrategy>(-1); | ||
| 71 | - const auto *strategy_name = GetStrategyName(invalid_strategy); | ||
| 72 | - const std::string reason = "Unknown merge strategy: algo=" + algo + ", strategy=" + strategy_name + | ||
| 73 | - " (expected LoadBalance or MainStream)."; | ||
| 74 | - (void)REPORT_PREDEFINED_ERR_MSG("E10001", std::vector<const char_t *>({"value", "parameter", "reason"}), | ||
| 75 | - std::vector<const char_t *>({algo.c_str(), readable.c_str(), reason.c_str()})); | ||
| 76 | - GELOGE(FAILED, "Unknown merge strategy in %s: algo=%s, strategy=%s (expected LoadBalance or MainStream).", | ||
| 77 | - readable.c_str(), algo.c_str(), strategy_name); | ||
| 78 | - return false; | ||
| 79 | - } | ||
| 80 | - | ||
| 81 | - std::string max_str = multi_stream_mode.substr(colon_pos + 1); | ||
| 82 | - int32_t max_val_int32 = 0; | ||
| 83 | - std::string range_msg = "Invalid max_stream value, must be in range [1, " + std::to_string(kMaxStreamLimit) + "]."; | ||
| 84 | - if (ge::ConvertToInt32(max_str, max_val_int32) != SUCCESS || max_val_int32 <= 0 || max_val_int32 > kMaxStreamLimit) { | ||
| 85 | - (void)REPORT_PREDEFINED_ERR_MSG( | ||
| 86 | - "E10001", std::vector<const char_t *>({"value", "parameter", "reason"}), | ||
| 87 | - std::vector<const char_t *>({max_str.c_str(), readable.c_str(), range_msg.c_str()})); | ||
| 88 | - GELOGE(FAILED, "Invalid max_stream value in %s: %s (must be in range [1, %d]).", readable.c_str(), max_str.c_str(), | ||
| 89 | - kMaxStreamLimit); | ||
| 90 | - return false; | ||
| 91 | - } | ||
| 92 | - | ||
| 93 | - out_max_stream_id = static_cast<int64_t>(max_val_int32) - 1; | ||
| 94 | - const auto *strategy_name = GetStrategyName(out_strategy); | ||
| 95 | - GELOGI("Parsed config: strategy=%s, max_stream_id=%ld.", strategy_name, out_max_stream_id); | ||
| 96 | - return true; | ||
| 97 | } | 49 | } |
| 98 | 50 | ||
| 99 | Status RunMiniDAGStreamPassForComputGraph(const ConstGraphPtr &graph, StreamPassContext &context, | 51 | Status RunMiniDAGStreamPassForComputGraph(const ConstGraphPtr &graph, StreamPassContext &context, |
| @@ -118,7 +70,7 @@ Status RunMiniDAGStreamPassForComputGraph(const ConstGraphPtr &graph, StreamPass | |||
| 118 | GELOGI("MiniDAGStreamPass graph %s final strategy=%s, reason=matched profiling node cost (override from %s).", | 70 | GELOGI("MiniDAGStreamPass graph %s final strategy=%s, reason=matched profiling node cost (override from %s).", |
| 119 | dag->GetName().c_str(), final_strategy_name, input_strategy_name); | 71 | dag->GetName().c_str(), final_strategy_name, input_strategy_name); |
| 120 | } else { | 72 | } else { |
| 121 | - GELOGD("MiniDAGStreamPass graph %s final strategy=%s, reason=no matched profiling node cost.", | 73 | + GELOGI("MiniDAGStreamPass graph %s final strategy=%s, reason=no matched profiling node cost.", |
| 122 | dag->GetName().c_str(), final_strategy_name); | 74 | dag->GetName().c_str(), final_strategy_name); |
| 123 | } | 75 | } |
| 124 | minidag::DagStreamAllocator::ByPathCover(*dag, config); | 76 | minidag::DagStreamAllocator::ByPathCover(*dag, config); |
| @@ -141,16 +93,32 @@ Status RunMiniDAGStreamPass(const ConstGraphPtr &graph, StreamPassContext &conte | |||
| 141 | // 1. 空图检查 | 93 | // 1. 空图检查 |
| 142 | GE_ASSERT_NOTNULL(graph); | 94 | GE_ASSERT_NOTNULL(graph); |
| 143 | 95 | ||
| 144 | - // 2. 读取 ge.autoMultistreamParallelMode(主配置) | 96 | + // 2. 读取图属性或 ge.autoMultistreamParallelMode option,图属性优先 |
| 97 | + const auto compute_graph = GraphUtilsEx::GetComputeGraph(*graph); | ||
| 98 | + GE_ASSERT_NOTNULL(compute_graph); | ||
| 145 | std::string multi_stream_mode; | 99 | std::string multi_stream_mode; |
| 146 | - GE_ASSERT_SUCCESS(GetContext().GetOption("ge.autoMultistreamParallelMode", multi_stream_mode), | 100 | + bool from_graph = false; |
| 147 | - "Failed to get ge.autoMultistreamParallelMode option"); | 101 | + if ((StreamUtils::GetAutoMultistreamParallelMode(compute_graph, multi_stream_mode, from_graph) != GRAPH_SUCCESS) || |
| 148 | - | 102 | + multi_stream_mode.empty()) { |
| 149 | - int64_t effective_max_stream_id = -1; | 103 | + GELOGI("MiniDAGStreamPass skipped: auto multistream parallel mode not set."); |
| 150 | - minidag::StreamMergeStrategy strategy; | 104 | + return SUCCESS; |
| 151 | - if (!ParseStreamConfig(multi_stream_mode, effective_max_stream_id, strategy)) { | 105 | + } |
| 106 | + AutoMultistreamConfig config; | ||
| 107 | + if (StreamUtils::ParseAutoMultistreamParallelMode(multi_stream_mode, config, from_graph) != GRAPH_SUCCESS) { | ||
| 152 | return FAILED; | 108 | return FAILED; |
| 153 | } | 109 | } |
| 110 | + if ((config.mode == AutoMultistreamMode::kDefault) || (config.mode == AutoMultistreamMode::kCv)) { | ||
| 111 | + GELOGI("MiniDAGStreamPass skipped for auto multistream parallel mode %s.", multi_stream_mode.c_str()); | ||
| 112 | + return SUCCESS; | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + if (!config.IsDagMode()) { | ||
| 116 | + return FAILED; | ||
| 117 | + } | ||
| 118 | + const int64_t effective_max_stream_id = static_cast<int64_t>(config.max_stream_num) - 1L; | ||
| 119 | + const auto strategy = ToMiniDagStrategy(config.mode); | ||
| 120 | + GELOGI("Auto multistream requested mode=%s, strategy=%s, max_stream_num=%d.", multi_stream_mode.c_str(), | ||
| 121 | + GetStrategyName(strategy), config.max_stream_num); | ||
| 154 | 122 | ||
| 155 | GE_ASSERT_SUCCESS(RunMiniDAGStreamPassForComputGraph(graph, context, effective_max_stream_id, strategy), | 123 | GE_ASSERT_SUCCESS(RunMiniDAGStreamPassForComputGraph(graph, context, effective_max_stream_id, strategy), |
| 156 | "root graph RunMiniDAGStreamPass failed"); | 124 | "root graph RunMiniDAGStreamPass failed"); |
| @@ -40,10 +40,13 @@ bool TopoOrderCompare(const NodePtr &n0, const NodePtr &n1) { | |||
| 40 | return (n0->GetOpDesc()->GetId() < n1->GetOpDesc()->GetId()); | 40 | return (n0->GetOpDesc()->GetId() < n1->GetOpDesc()->GetId()); |
| 41 | } | 41 | } |
| 42 | 42 | ||
| 43 | -bool IsAutoMultistreamModeEnabled() { | 43 | +bool IsAutoMultistreamModeEnabled(const ComputeGraphPtr &graph) { |
| 44 | std::string multi_stream_mode; | 44 | std::string multi_stream_mode; |
| 45 | - return (GetContext().GetOption("ge.autoMultistreamParallelMode", multi_stream_mode) == GRAPH_SUCCESS) && | 45 | + bool from_graph = false; |
| 46 | - (!multi_stream_mode.empty()) && (multi_stream_mode != "cv"); | 46 | + AutoMultistreamConfig config; |
| 47 | + return (StreamUtils::GetAutoMultistreamParallelMode(graph, multi_stream_mode, from_graph) == GRAPH_SUCCESS) && | ||
| 48 | + (StreamUtils::ParseAutoMultistreamParallelMode(multi_stream_mode, config, from_graph) == GRAPH_SUCCESS) && | ||
| 49 | + config.IsDagMode(); | ||
| 47 | } | 50 | } |
| 48 | 51 | ||
| 49 | } // namespace | 52 | } // namespace |
| @@ -120,7 +123,7 @@ Status DynamicStreamAllocator::AssignStreams(const ComputeGraphPtr &root_graph, | |||
| 120 | GE_ASSERT_SUCCESS(RefreshContinuousStreams(root_graph)); | 123 | GE_ASSERT_SUCCESS(RefreshContinuousStreams(root_graph)); |
| 121 | 124 | ||
| 122 | GE_ASSERT_SUCCESS(StreamUtils::RunCustomStreamPass(root_graph, stream_num_)); | 125 | GE_ASSERT_SUCCESS(StreamUtils::RunCustomStreamPass(root_graph, stream_num_)); |
| 123 | - if (IsAutoMultistreamModeEnabled()) { | 126 | + if (IsAutoMultistreamModeEnabled(root_graph)) { |
| 124 | GE_ASSERT_SUCCESS(RefreshContinuousStreamsByNodeIds(root_graph)); | 127 | GE_ASSERT_SUCCESS(RefreshContinuousStreamsByNodeIds(root_graph)); |
| 125 | } | 128 | } |
| 126 | return SUCCESS; | 129 | return SUCCESS; |
| @@ -404,7 +404,8 @@ void AssignByDependencyPass::UpdateReusedSubgraphs() { | |||
| 404 | 404 | ||
| 405 | Status SingleStreamPass::Run(ComputeGraphPtr graph, const std::vector<SubgraphPtr> &subgraphs, Context &context) { | 405 | Status SingleStreamPass::Run(ComputeGraphPtr graph, const std::vector<SubgraphPtr> &subgraphs, Context &context) { |
| 406 | std::string auto_multi_stream_mode; | 406 | std::string auto_multi_stream_mode; |
| 407 | - (void)GetContext().GetOption(OPTION_AUTO_MULTISTREAM_PARALLEL_MODE, auto_multi_stream_mode); | 407 | + bool from_graph = false; |
| 408 | + (void)StreamUtils::GetAutoMultistreamParallelMode(graph, auto_multi_stream_mode, from_graph); | ||
| 408 | if (!auto_multi_stream_mode.empty()) { | 409 | if (!auto_multi_stream_mode.empty()) { |
| 409 | const std::string auto_multi_stream_name = GetContext().GetReadableName(OPTION_AUTO_MULTISTREAM_PARALLEL_MODE); | 410 | const std::string auto_multi_stream_name = GetContext().GetReadableName(OPTION_AUTO_MULTISTREAM_PARALLEL_MODE); |
| 410 | const std::string single_stream_name = GetContext().GetReadableName(ENABLE_SINGLE_STREAM); | 411 | const std::string single_stream_name = GetContext().GetReadableName(ENABLE_SINGLE_STREAM); |
| @@ -415,7 +416,6 @@ Status SingleStreamPass::Run(ComputeGraphPtr graph, const std::vector<SubgraphPt | |||
| 415 | return PARAM_INVALID; | 416 | return PARAM_INVALID; |
| 416 | } | 417 | } |
| 417 | 418 | ||
| 418 | - (void)graph; | ||
| 419 | // context.default_stream can be kInvalidStream only when graph is the root graph. | 419 | // context.default_stream can be kInvalidStream only when graph is the root graph. |
| 420 | int64_t new_stream = context.default_stream; | 420 | int64_t new_stream = context.default_stream; |
| 421 | if (new_stream == kInvalidStream) { | 421 | if (new_stream == kInvalidStream) { |
| @@ -10,6 +10,9 @@ | |||
| 10 | 10 | ||
| 11 | 11 | ||
| 12 | 12 | ||
| 13 | + | ||
| 14 | + | ||
| 15 | + | ||
| 13 | 16 | ||
| 14 | 17 | ||
| 15 | 18 | ||
| @@ -23,6 +26,8 @@ | |||
| 23 | namespace { | 26 | namespace { |
| 24 | constexpr const ge::char_t *const kTrueStr = "true"; | 27 | constexpr const ge::char_t *const kTrueStr = "true"; |
| 25 | constexpr const ge::char_t *const kFalseStr = "false"; | 28 | constexpr const ge::char_t *const kFalseStr = "false"; |
| 29 | +constexpr const ge::char_t *const kAutoMultistreamParallelModeOption = "ge.autoMultistreamParallelMode"; | ||
| 30 | +constexpr int32_t kMaxAutoMultistreamNum = 64; | ||
| 26 | const std::set<std::string> hccl_op_types({ge::HCOMBROADCAST, ge::HCOMALLGATHER, ge::HCOMALLREDUCE, | 31 | const std::set<std::string> hccl_op_types({ge::HCOMBROADCAST, ge::HCOMALLGATHER, ge::HCOMALLREDUCE, |
| 27 | ge::HCOMREDUCESCATTER, ge::HCOMREDUCE, ge::HCOMALLTOALLV, | 32 | ge::HCOMREDUCESCATTER, ge::HCOMREDUCE, ge::HCOMALLTOALLV, |
| 28 | ge::HCOMGATHERALLTOALLV, ge::HCOMALLTOALLVC, ge::HCOMALLTOALL}); | 33 | ge::HCOMGATHERALLTOALLV, ge::HCOMALLTOALLVC, ge::HCOMALLTOALL}); |
| @@ -32,6 +37,16 @@ namespace ge { | |||
| 32 | std::mutex StreamUtils::mutex_; | 37 | std::mutex StreamUtils::mutex_; |
| 33 | std::map<std::string, PriorityEnum> StreamUtils::engine_priority_; | 38 | std::map<std::string, PriorityEnum> StreamUtils::engine_priority_; |
| 34 | 39 | ||
| 40 | +namespace { | ||
| 41 | +graphStatus ReportInvalidAutoMultistreamMode(const std::string &value, const std::string &reason) { | ||
| 42 | + const auto readable = GetContext().GetReadableName(kAutoMultistreamParallelModeOption); | ||
| 43 | + (void)REPORT_PREDEFINED_ERR_MSG("E10001", std::vector<const char_t *>({"value", "parameter", "reason"}), | ||
| 44 | + std::vector<const char_t *>({value.c_str(), readable.c_str(), reason.c_str()})); | ||
| 45 | + GELOGE(FAILED, "%s is invalid, value=%s, reason=%s", readable.c_str(), value.c_str(), reason.c_str()); | ||
| 46 | + return GRAPH_FAILED; | ||
| 47 | +} | ||
| 48 | +} // namespace | ||
| 49 | + | ||
| 35 | Status StreamUtils::ConvertSubgraphs(const ComputeGraphPtr &graph, const Graph2SubGraphInfoList &subgraph_map, | 50 | Status StreamUtils::ConvertSubgraphs(const ComputeGraphPtr &graph, const Graph2SubGraphInfoList &subgraph_map, |
| 36 | const std::map<std::string, EngineConfPtr> &engine_confs, | 51 | const std::map<std::string, EngineConfPtr> &engine_confs, |
| 37 | const std::map<std::string, int32_t> &max_parallel_num, | 52 | const std::map<std::string, int32_t> &max_parallel_num, |
| @@ -258,10 +273,88 @@ bool StreamUtils::EnableDynamicShapeMultiStream() { | |||
| 258 | return false; | 273 | return false; |
| 259 | } | 274 | } |
| 260 | 275 | ||
| 261 | -bool StreamUtils::EnableCvParallel() { | 276 | +graphStatus StreamUtils::GetAutoMultistreamParallelMode(std::string &multi_stream_mode) { |
| 277 | + multi_stream_mode.clear(); | ||
| 278 | + const auto ret = GetContext().GetOption(kAutoMultistreamParallelModeOption, multi_stream_mode); | ||
| 279 | + return ((ret == GRAPH_SUCCESS) && (!multi_stream_mode.empty())) ? GRAPH_SUCCESS : ret; | ||
| 280 | +} | ||
| 281 | + | ||
| 282 | +graphStatus StreamUtils::GetAutoMultistreamParallelMode(const ComputeGraphPtr &graph, std::string &multi_stream_mode, | ||
| 283 | + bool &from_graph) { | ||
| 284 | + multi_stream_mode.clear(); | ||
| 285 | + from_graph = false; | ||
| 286 | + | ||
| 287 | + if (graph != nullptr) { | ||
| 288 | + const auto root_graph = GraphUtils::FindRootGraph(graph); | ||
| 289 | + if ((root_graph != nullptr) && | ||
| 290 | + AttrUtils::GetStr(root_graph, kAutoMultistreamParallelModeOption, multi_stream_mode)) { | ||
| 291 | + from_graph = true; | ||
| 292 | + GELOGI("Use auto multistream parallel mode %s from graph attribute.", multi_stream_mode.c_str()); | ||
| 293 | + return GRAPH_SUCCESS; | ||
| 294 | + } | ||
| 295 | + } | ||
| 296 | + | ||
| 297 | + const auto ret = GetContext().GetOption(kAutoMultistreamParallelModeOption, multi_stream_mode); | ||
| 298 | + return ((ret == GRAPH_SUCCESS) && (!multi_stream_mode.empty())) ? GRAPH_SUCCESS : ret; | ||
| 299 | +} | ||
夏 | |||
| 300 | + | ||
| 301 | +graphStatus StreamUtils::ParseAutoMultistreamParallelMode(const std::string &multi_stream_mode, | ||
| 302 | + AutoMultistreamConfig &config, const bool from_graph) { | ||
| 303 | + config = AutoMultistreamConfig{}; | ||
| 304 | + if (multi_stream_mode.empty()) { | ||
| 305 | + return GRAPH_SUCCESS; | ||
| 306 | + } | ||
| 307 | + if (multi_stream_mode == "default") { | ||
| 308 | + if (!from_graph) { | ||
| 309 | + return ReportInvalidAutoMultistreamMode( | ||
| 310 | + multi_stream_mode, "The default mode is supported only by the graph attribute set by a custom pass."); | ||
| 311 | + } | ||
| 312 | + config.mode = AutoMultistreamMode::kDefault; | ||
| 313 | + return GRAPH_SUCCESS; | ||
| 314 | + } | ||
| 315 | + if (multi_stream_mode == "cv") { | ||
| 316 | + config.mode = AutoMultistreamMode::kCv; | ||
| 317 | + return GRAPH_SUCCESS; | ||
| 318 | + } | ||
| 319 | + | ||
| 320 | + const auto colon_pos = multi_stream_mode.find(':'); | ||
| 321 | + if ((colon_pos == std::string::npos) || (colon_pos != multi_stream_mode.rfind(':'))) { | ||
| 322 | + return ReportInvalidAutoMultistreamMode(multi_stream_mode, "The format must be Strategy:N with one colon."); | ||
| 323 | + } | ||
| 324 | + | ||
| 325 | + const std::string strategy = multi_stream_mode.substr(0U, colon_pos); | ||
| 326 | + if (strategy == "LoadBalance") { | ||
| 327 | + config.mode = AutoMultistreamMode::kLoadBalance; | ||
| 328 | + } else if (strategy == "MainStream") { | ||
| 329 | + config.mode = AutoMultistreamMode::kMainStream; | ||
| 330 | + } else if (strategy == "WeightedLoadBalance") { | ||
| 331 | + config.mode = AutoMultistreamMode::kWeightedLoadBalance; | ||
| 332 | + } else { | ||
| 333 | + return ReportInvalidAutoMultistreamMode(multi_stream_mode, | ||
| 334 | + "The strategy must be LoadBalance, MainStream, or WeightedLoadBalance."); | ||
| 335 | + } | ||
| 336 | + | ||
| 337 | + const std::string max_stream_str = multi_stream_mode.substr(colon_pos + 1U); | ||
| 338 | + const bool is_decimal = | ||
| 339 | + !max_stream_str.empty() && std::all_of(max_stream_str.begin(), max_stream_str.end(), [](const char_t value) { | ||
| 340 | + return std::isdigit(static_cast<unsigned char>(value)) != 0; | ||
| 341 | + }); | ||
| 342 | + int32_t max_stream_num = 0; | ||
| 343 | + if ((!is_decimal) || (ConvertToInt32(max_stream_str, max_stream_num) != SUCCESS) || (max_stream_num < 1) || | ||
| 344 | + (max_stream_num > kMaxAutoMultistreamNum)) { | ||
| 345 | + return ReportInvalidAutoMultistreamMode(multi_stream_mode, "The stream number must be an integer in [1, 64]."); | ||
| 346 | + } | ||
| 347 | + config.max_stream_num = max_stream_num; | ||
| 348 | + return GRAPH_SUCCESS; | ||
| 349 | +} | ||
| 350 | + | ||
| 351 | +bool StreamUtils::EnableCvParallel(const ComputeGraphPtr &graph) { | ||
| 262 | std::string multi_stream_mode; | 352 | std::string multi_stream_mode; |
| 263 | - if ((ge::GetContext().GetOption("ge.autoMultistreamParallelMode", multi_stream_mode) == ge::GRAPH_SUCCESS) && | 353 | + bool from_graph = false; |
| 264 | - (multi_stream_mode == "cv")) { | 354 | + AutoMultistreamConfig config; |
| 355 | + if ((GetAutoMultistreamParallelMode(graph, multi_stream_mode, from_graph) == GRAPH_SUCCESS) && | ||
| 356 | + (ParseAutoMultistreamParallelMode(multi_stream_mode, config, from_graph) == GRAPH_SUCCESS) && | ||
| 357 | + (config.mode == AutoMultistreamMode::kCv)) { | ||
| 265 | GELOGI("auto multistream parallel mode is %s", multi_stream_mode.c_str()); | 358 | GELOGI("auto multistream parallel mode is %s", multi_stream_mode.c_str()); |
| 266 | return true; | 359 | return true; |
| 267 | } | 360 | } |
| @@ -25,6 +25,25 @@ constexpr int64_t kInvalidStream = -1; | |||
| 25 | constexpr int64_t kMainStream = 0; | 25 | constexpr int64_t kMainStream = 0; |
| 26 | constexpr int64_t kDefaultMaxParalleNum = 1; | 26 | constexpr int64_t kDefaultMaxParalleNum = 1; |
| 27 | 27 | ||
| 28 | +enum class AutoMultistreamMode : uint32_t { | ||
| 29 | + kUnset = 0U, | ||
| 30 | + kDefault, | ||
| 31 | + kCv, | ||
| 32 | + kLoadBalance, | ||
| 33 | + kMainStream, | ||
| 34 | + kWeightedLoadBalance | ||
| 35 | +}; | ||
| 36 | + | ||
| 37 | +struct AutoMultistreamConfig { | ||
| 38 | + AutoMultistreamMode mode = AutoMultistreamMode::kUnset; | ||
| 39 | + int32_t max_stream_num = 0; | ||
| 40 | + | ||
| 41 | + bool IsDagMode() const { | ||
| 42 | + return (mode == AutoMultistreamMode::kLoadBalance) || (mode == AutoMultistreamMode::kMainStream) || | ||
| 43 | + (mode == AutoMultistreamMode::kWeightedLoadBalance); | ||
| 44 | + } | ||
| 45 | +}; | ||
| 46 | + | ||
| 28 | struct Subgraph { | 47 | struct Subgraph { |
| 29 | std::string name; | 48 | std::string name; |
| 30 | int64_t stream_id = kInvalidStream; | 49 | int64_t stream_id = kInvalidStream; |
| @@ -66,7 +85,12 @@ class StreamUtils { | |||
| 66 | 85 | ||
| 67 | static bool EnableSingleStream(); | 86 | static bool EnableSingleStream(); |
| 68 | static bool EnableDynamicShapeMultiStream(); | 87 | static bool EnableDynamicShapeMultiStream(); |
| 69 | - static bool EnableCvParallel(); | 88 | + static graphStatus GetAutoMultistreamParallelMode(std::string &multi_stream_mode); |
| 89 | + static graphStatus GetAutoMultistreamParallelMode(const ComputeGraphPtr &graph, std::string &multi_stream_mode, | ||
| 90 | + bool &from_graph); | ||
| 91 | + static graphStatus ParseAutoMultistreamParallelMode(const std::string &multi_stream_mode, | ||
| 92 | + AutoMultistreamConfig &config, bool from_graph = false); | ||
| 93 | + static bool EnableCvParallel(const ComputeGraphPtr &graph); | ||
| 70 | static bool IsAivNode(const NodePtr &node); | 94 | static bool IsAivNode(const NodePtr &node); |
| 71 | static bool IsAicNode(const NodePtr &node); | 95 | static bool IsAicNode(const NodePtr &node); |
| 72 | 96 | ||
| @@ -125,7 +125,7 @@ bool IsLinkedInGraph(const NodePtr &src_node, const NodePtr &dst_node) { | |||
| 125 | 125 | ||
| 126 | // 根据topo序找到aiv前后相邻的aic节点,如果aiv和其中至少1个aic在图上没有通路则可以并发 | 126 | // 根据topo序找到aiv前后相邻的aic节点,如果aiv和其中至少1个aic在图上没有通路则可以并发 |
| 127 | void MarkCvParallelAivNodes(const ComputeGraphPtr &graph) { | 127 | void MarkCvParallelAivNodes(const ComputeGraphPtr &graph) { |
| 128 | - if (!StreamUtils::EnableCvParallel()) { | 128 | + if (!StreamUtils::EnableCvParallel(graph)) { |
| 129 | return; | 129 | return; |
| 130 | } | 130 | } |
| 131 | std::map<NodePtr, std::pair<NodePtr, NodePtr>> aiv_to_adjacent_aic_nodes; | 131 | std::map<NodePtr, std::pair<NodePtr, NodePtr>> aiv_to_adjacent_aic_nodes; |
| @@ -13,3 +13,4 @@ | |||
| 13 | | 推荐网络高性能推理样例 | [README](recommendation/README.md) | | 13 | | 推荐网络高性能推理样例 | [README](recommendation/README.md) | |
| 14 | | 自定义算子入图样例 | [README](custom_op/README.md) | | 14 | | 自定义算子入图样例 | [README](custom_op/README.md) | |
| 15 | | YOLOv13目标检测样例 | [README](acl/5_sample_yolov13/README.md) | | 15 | | YOLOv13目标检测样例 | [README](acl/5_sample_yolov13/README.md) | |
| 16 | +| 多流自动寻优样例 | [README](multi_stream_autotune/README.md) | | ||
| @@ -13,3 +13,4 @@ This project provides call samples for different scenarios. After setting up the | |||
| 13 | | Recommendation network high-performance inference sample | [README](recommendation/README.md) | | 13 | | Recommendation network high-performance inference sample | [README](recommendation/README.md) | |
| 14 | | Custom operator graph integration sample | [README](custom_op/README_en.md) | | 14 | | Custom operator graph integration sample | [README](custom_op/README_en.md) | |
| 15 | | YOLOv13 object detection sample | [README](acl/5_sample_yolov13/README_en.md) | | 15 | | YOLOv13 object detection sample | [README](acl/5_sample_yolov13/README_en.md) | |
| 16 | +| Multi-stream autotune sample | [README](multi_stream_autotune/README_en.md) | | ||
| @@ -0,0 +1,340 @@ | |||
| 1 | +# GE 多流自动寻优样例 | ||
| 2 | + | ||
| 3 | +[English](README_en.md) | ||
| 4 | + | ||
| 5 | +GE 的自动多流提供多种流分配策略(`LoadBalance`、`MainStream`、`WeightedLoadBalance`、`cv`), | ||
| 6 | +不同策略与流数组合对端到端耗时影响显著,且与模型结构、芯片型号强相关,只能实测选优。 | ||
| 7 | + | ||
| 8 | +本样例提供最小可用的寻优闭环:**用环境变量下发候选配置 → 自定义 Pass 写入根图属性 → | ||
| 9 | +GE 打点输出每步耗时 → 驱动脚本统计排名并给出推荐配置**。 | ||
| 10 | + | ||
| 11 | +## 能力边界 | ||
| 12 | + | ||
| 13 | +本样例做的事: | ||
| 14 | + | ||
| 15 | +- 提供一个通用自定义 Pass,把候选配置写入根图属性,被测样例无需改代码; | ||
| 16 | +- 提供一个寻优驱动脚本,遍历候选、反复执行、解析 STEP 日志、排名并推荐配置; | ||
| 17 | +- 支持两种执行方式:**在线**(本机跑被测命令)与**离线**(编译机逐候选编 OM → | ||
| 18 | + 传到目标机 → 远端执行 → 回传 plog → 统一解析); | ||
| 19 | +- 提供一个最小被测样例,用于验证整条链路是否打通。 | ||
| 20 | + | ||
| 21 | +本样例不做的事: | ||
| 22 | + | ||
| 23 | +- 不校验各候选的计算结果一致性(需要时在被测样例里自行比对); | ||
| 24 | +- 不支持断点续跑,中断后需重跑(可用 `--configs` 拆批降低损失); | ||
| 25 | +- 不管理 CANN 环境安装与设备独占。 | ||
| 26 | + | ||
| 27 | +## 目录结构 | ||
| 28 | + | ||
| 29 | +``` | ||
| 30 | +multi_stream_autotune/ | ||
| 31 | +├── README.md / README_en.md | ||
| 32 | +├── ge_ms_autotune.py 寻优驱动:遍历候选、解析 STEP、排名推荐 | ||
| 33 | +├── sample_run.py 最小被测样例:多分支静态图 + Session 反复执行 | ||
| 34 | +└── custom_pass/ 通用自定义 Pass:环境变量 → 根图属性 | ||
| 35 | + ├── CMakeLists.txt | ||
| 36 | + └── src/ge_ms_autotune_pass.cpp | ||
| 37 | +``` | ||
| 38 | + | ||
| 39 | +## 原理 | ||
| 40 | + | ||
| 41 | +配置下发链路: | ||
| 42 | + | ||
| 43 | +``` | ||
| 44 | +ge_ms_autotune.py --(GE_AUTO_MULTISTREAM_PARALLEL_MODE=LoadBalance:4)--> 被测进程 | ||
| 45 | + │ | ||
| 46 | + custom_pass 在 kBeforeInferShape 阶段读取环境变量 │ | ||
| 47 | + ▼ | ||
| 48 | + 根图属性 ge.autoMultistreamParallelMode = LoadBalance:4 | ||
| 49 | + _auto_multistream_tuning_mode = LoadBalance:4 | ||
| 50 | + │ | ||
| 51 | + ▼ | ||
| 52 | + GE 按该属性做流分配,并对每次执行输出 STEP 打点 | ||
| 53 | +``` | ||
| 54 | + | ||
| 55 | +几个关键点: | ||
| 56 | + | ||
| 57 | +- **GE 不读取 `GE_AUTO_MULTISTREAM_PARALLEL_MODE`**,该变量只是本样例 Pass 与驱动脚本之间的约定, | ||
| 58 | + 作用是在不改被测样例的前提下逐候选切换配置。 | ||
| 59 | +- **图属性优先于同名 option**,`ge.autoMultistreamParallelMode` 图属性存在时, | ||
| 60 | + 会覆盖 Session/ATC 传入的同名 option。 | ||
| 61 | +- **`_auto_multistream_tuning_mode` 是调测身份属性**,只有它存在时 GE 才输出 STEP 打点。 | ||
| 62 | + 该属性随 GeModel 保存进 OM,因此离线执行同样能打点。 | ||
| 63 | +- **打点含同步等待**(执行器按实际 stream 同步界定完成边界),只用于寻优调测, | ||
| 64 | + 生产态不要开启。 | ||
| 65 | +- **`default` 也是一个候选**,表示不启用自动多流的基准,同样由图属性下发,用于算加速比。 | ||
| 66 | + | ||
| 67 | +## 前置条件 | ||
| 68 | + | ||
| 69 | +- 与被测 GE 配套的 CANN,且包含图属性自动多流与 STEP 打点能力; | ||
| 70 | +- 可用的昇腾 NPU 设备,测试期间关闭 profiling、尽量独占设备; | ||
| 71 | +- 编译 Pass 需要 CMake 3.13+、支持 C++17 的编译器,以及 `$ASCEND_HOME_PATH/include/register/register_custom_pass.h`; | ||
| 72 | +- 驱动脚本只依赖 Python 3.7+ 标准库。 | ||
| 73 | + | ||
| 74 | +```bash | ||
| 75 | +source /path/to/cann/set_env.sh | ||
| 76 | +export ASCEND_HOME_PATH=/path/to/cann | ||
| 77 | +test -f "$ASCEND_HOME_PATH/include/register/register_custom_pass.h" | ||
| 78 | +``` | ||
| 79 | + | ||
| 80 | +## 步骤一:编译并安装寻优 Pass | ||
| 81 | + | ||
| 82 | +```bash | ||
| 83 | +cd examples/multi_stream_autotune | ||
| 84 | +cmake -S custom_pass -B build -DASCEND_HOME_PATH="${ASCEND_HOME_PATH}" | ||
| 85 | +cmake --build build --parallel | ||
| 86 | + | ||
| 87 | +# 安装到 GE 扫描的自定义 Pass 目录(vendors 下的目录名可自取) | ||
| 88 | +PASS_DIR="${ASCEND_OPP_PATH:-$ASCEND_HOME_PATH/opp}/vendors/ge_ms_autotune/custom_fusion_passes" | ||
| 89 | +mkdir -p "${PASS_DIR}" | ||
| 90 | +install -m 750 build/libge_ms_autotune_pass.so "${PASS_DIR}/" | ||
| 91 | +``` | ||
| 92 | + | ||
| 93 | +注意: | ||
| 94 | + | ||
| 95 | +- 该目录下若已有其他会写多流图属性的 Pass,请先移走,否则属性会被互相覆盖; | ||
| 96 | +- **寻优结束后请删除该 so**,避免调测打点与强制属性带到生产环境: | ||
| 97 | + `rm -f "${PASS_DIR}/libge_ms_autotune_pass.so"`。 | ||
| 98 | + | ||
| 99 | +## 步骤二:准备被测样例 | ||
| 100 | + | ||
| 101 | +被测样例(即寻优时反复执行、用来比较耗时的那个程序)可以是任意在进程内完成编图并反复执行的 | ||
| 102 | +命令,需满足: | ||
| 103 | + | ||
| 104 | +- 每次进程启动都重新编图,不复用上一个候选留下的图/模型缓存; | ||
| 105 | +- 各候选使用完全相同的输入与迭代次数,warmup 之后至少执行 `--min-steps` 次; | ||
| 106 | +- 通过退出码反馈成败(非 0 的轮次不参与排名)。 | ||
| 107 | + | ||
| 108 | +`sample_run.py` 是一个参照实现:构造四条互不依赖的 pointwise 分支的静态图(多流下可分派到 | ||
| 109 | +不同流),预热 1 步后执行 `--steps` 轮: | ||
| 110 | + | ||
| 111 | +```bash | ||
| 112 | +python3 sample_run.py --steps 12 --dim 512 | ||
| 113 | +``` | ||
| 114 | + | ||
| 115 | +## 步骤三:运行寻优 | ||
| 116 | + | ||
| 117 | +```bash | ||
| 118 | +python3 ge_ms_autotune.py \ | ||
| 119 | + --run-command "python3 sample_run.py --steps 12" \ | ||
| 120 | + --strategies LoadBalance,MainStream \ | ||
| 121 | + --streams 2,4,8 \ | ||
| 122 | + --repeat 3 \ | ||
| 123 | + --output-dir ./tune_out | ||
| 124 | +``` | ||
| 125 | + | ||
| 126 | +驱动脚本对每个候选:注入 `GE_AUTO_MULTISTREAM_PARALLEL_MODE` 和独立的 `ASCEND_PROCESS_LOG_PATH` | ||
| 127 | +→ 执行命令 → 从 stdout 与该轮 plog 中收集 STEP → 校验后统计。控制台输出形如: | ||
| 128 | + | ||
| 129 | +``` | ||
| 130 | +候选配置(7 个 × 3 轮):default, LoadBalance:2, LoadBalance:4, ... | ||
| 131 | + | ||
| 132 | +[000] 配置=default 第 1 轮:python3 sample_run.py --steps 12 | ||
| 133 | + 退出码=0 STEP=13 有效=是 耗时=21.4s | ||
| 134 | +... | ||
| 135 | + | ||
| 136 | +寻优结果(按中位耗时升序): | ||
| 137 | +配置 有效轮次 步数 平均(ms) 中位(ms) P90(ms) CV 加速比 结论 | ||
| 138 | +LoadBalance:4 3/3 36 12.104 12.088 12.301 0.014 1.243 提升 | ||
| 139 | +MainStream:4 3/3 36 13.552 13.489 13.702 0.011 1.114 提升 | ||
| 140 | +default 3/3 36 15.037 15.028 15.311 0.009 1.000 持平 | ||
| 141 | + | ||
| 142 | +[结论] 推荐配置:LoadBalance:4,相对 default 加速比 1.243,中位耗时 12.088 ms。 | ||
| 143 | +[复现] GE_AUTO_MULTISTREAM_PARALLEL_MODE=LoadBalance:4 python3 sample_run.py --steps 12 | ||
| 144 | +``` | ||
| 145 | + | ||
| 146 | +一阶段扫完后若想细化流数,指定相邻取值再跑一次即可: | ||
| 147 | + | ||
| 148 | +```bash | ||
| 149 | +python3 ge_ms_autotune.py --run-command "..." \ | ||
| 150 | + --configs default,LoadBalance:3,LoadBalance:4,LoadBalance:5 --output-dir ./tune_out_stage2 | ||
| 151 | +``` | ||
| 152 | + | ||
| 153 | +## 参数说明 | ||
| 154 | + | ||
| 155 | +| 参数 | 默认值 | 说明 | | ||
| 156 | +|---|---|---| | ||
| 157 | +| `--mode` | `online` | `online` 本机执行;`offline` 编译 OM 后送目标机执行 | | ||
| 158 | +| `--run-command` | online 必填 | 被测命令,整体加引号;按 shell 词法切分后直接执行,不经过 shell | | ||
| 159 | +| `--compile-command` | offline 必填 | 编译 OM 的命令,用 `{om}`(含 `.om`)或 `{om_prefix}`(不含后缀)占位输出路径 | | ||
| 160 | +| `--target` | offline 必填 | 目标机配置 JSON 路径,字段见[离线场景](#离线场景目标机执行) | | ||
| 161 | +| `--om-dir` | `<output-dir>/om` | offline:OM 产物与编译日志的存放目录 | | ||
| 162 | +| `--strategies` | `LoadBalance,MainStream` | 候选策略,逗号分隔,可选 `LoadBalance`/`MainStream`/`WeightedLoadBalance`/`cv` | | ||
| 163 | +| `--streams` | `2,4,8` | 候选流数,逗号分隔,取值 `[1,64]`;`cv` 策略不带流数 | | ||
| 164 | +| `--configs` | 空 | 直接给定候选(如 `default,LoadBalance:4`),指定后忽略上面两个矩阵参数 | | ||
| 165 | +| `--repeat` | `3` | 每个候选重复轮数,正式比较建议不少于 3 | | ||
| 166 | +| `--drop-first` | `1` | 丢弃前若干个 STEP(预热) | | ||
| 167 | +| `--min-steps` | `5` | 单轮有效 STEP 数下限,低于该值判为无效 | | ||
| 168 | +| `--main-graph` | 自动 | 多执行对象时指定主对象:`session_id:graph_id` 或 `model:model_id`;默认取 STEP 数最多者 | | ||
| 169 | +| `--timeout` | `1800` | 单轮超时秒数,`0` 表示不限制 | | ||
| 170 | +| `--output-dir` | `./ge_ms_autotune_output` | 结果目录,必须不存在或为空 | | ||
| 171 | + | ||
| 172 | +`default` 基准会自动加入候选列表并排在首位。 | ||
| 173 | + | ||
| 174 | +## 输出与结果解读 | ||
| 175 | + | ||
| 176 | +``` | ||
| 177 | +tune_out/ | ||
| 178 | +├── summary.csv / summary.json 候选汇总(含各轮明细与无效原因) | ||
| 179 | +├── om/ 仅 offline:各候选 OM 与编译日志 | ||
| 180 | +├── target_*.log 仅 offline:远端目录准备、上传、清理日志 | ||
| 181 | +└── trial_000_default_r1/ | ||
| 182 | + ├── stdout.log 被测命令(offline 为远端 ssh 会话)的 stdout+stderr | ||
| 183 | + ├── steps.csv 本轮解析出的全部 STEP | ||
| 184 | + ├── fetch_plog.log 仅 offline:plog 回传日志 | ||
| 185 | + └── plog/ 本轮 GE 日志(offline 为目标机回传的副本) | ||
| 186 | +``` | ||
| 187 | + | ||
| 188 | +统计口径与推荐规则: | ||
| 189 | + | ||
| 190 | +- 每个候选把各有效轮次主执行对象的步骤耗时合并后统计,排名按**中位耗时**升序; | ||
| 191 | +- 加速比 = `default 中位耗时 / 候选中位耗时`; | ||
| 192 | +- `≥1.05` 记为「提升」,`[0.98, 1.05)` 记为「持平」,`<0.98` 记为「劣化」; | ||
| 193 | +- 推荐中位耗时最小且加速比 `≥1.05` 的候选;没有这样的候选时建议保持 `default`; | ||
| 194 | +- `CV`(变异系数)只用于判断数据稳定程度,不参与排名;`CV > 0.05` 时会提示增大 `--repeat` 复测。 | ||
| 195 | + | ||
| 196 | +## 数据有效性门禁 | ||
| 197 | + | ||
| 198 | +命中任一条则该轮不参与排名,并在控制台与 `summary.json` 中给出原因: | ||
| 199 | + | ||
| 200 | +| 检查项 | 说明 | | ||
| 201 | +|---|---| | ||
| 202 | +| 退出码非 0 | 被测命令失败或超时 | | ||
| 203 | +| 日志异常 | STEP 行缺字段、字段非整数、`cost_us` 与时间区间不一致、执行身份缺失或混用 | | ||
| 204 | +| `mode` 不匹配 | STEP 里的 `mode` 与当前候选不一致,通常说明 Pass 未安装或被其他 Pass 覆盖 | | ||
| 205 | +| `ret`/`sync_ret` 非 0 | 执行接口或同步接口返回失败 | | ||
| 206 | +| 有效步数不足 | 丢弃预热后主执行对象的 STEP 少于 `--min-steps` | | ||
| 207 | +| 时间区间重叠 | 主执行对象的 STEP 区间相互重叠,说明并发提交,不能按串行耗时统计 | | ||
| 208 | + | ||
| 209 | +> 各候选的计算结果一致性不在检查范围内,需要时请在被测样例里自行校验(例如固定输入并比对输出摘要)。 | ||
| 210 | + | ||
| 211 | +## STEP 日志格式 | ||
| 212 | + | ||
| 213 | +打点位于执行器内部,在线与离线共用同一批位置,均以 `model_id` 标识执行对象: | ||
| 214 | + | ||
| 215 | +``` | ||
| 216 | +[EVENT] GE(pid,proc): [GE_MS_TUNE][STEP] api=NnExecute mode=LoadBalance:4 \ | ||
| 217 | + model_id=7 step=3 start_us=100 end_us=140 cost_us=40 sync_us=0 ret=0 sync_ret=0 | ||
| 218 | +``` | ||
| 219 | + | ||
| 220 | +| 字段 | 说明 | | ||
| 221 | +|---|---| | ||
| 222 | +| `api` | 打点位置,取值 `NnExecute`/`Run`(静态 shape)、`ModelV2Executor`(RT2.0 动态 shape) | | ||
| 223 | +| `mode` | 本次执行生效的多流配置,用于反查候选是否真的下发成功 | | ||
| 224 | +| `session_id`+`graph_id` / `model_id` | 执行对象身份,二选一;当前执行侧统一输出 `model_id` | | ||
| 225 | +| `step` | 步骤序号,从 0 开始 | | ||
| 226 | +| `start_us`/`end_us`/`cost_us` | 步骤起止与耗时(微秒),`cost_us = end_us - start_us` | | ||
| 227 | +| `sync_us` | 其中的同步等待耗时(微秒) | | ||
| 228 | +| `ret`/`sync_ret` | 执行返回值与同步返回值,0 为成功 | | ||
| 229 | + | ||
| 230 | +统计口径为「任务下发 → 流同步完成」,不含 H2D/D2H 拷贝与接口层开销,因此数值小于端到端单步耗时。 | ||
| 231 | + | ||
| 232 | +覆盖范围:静态 shape(`DavinciModel`,含队列异步 worker)与 RT2.0 动态 shape(`ModelV2Executor`) | ||
| 233 | +两条执行栈,在线与离线均覆盖;不覆盖 `aclmdlExecuteAsyncV2` 与 DFlow 执行链路。 | ||
| 234 | + | ||
| 235 | +已日落的 RT1.0 动态 shape 执行器(`HybridModelRtV1Executor`)与 RtV2Pipeline 执行器不打点或数值不可信; | ||
| 236 | +**OM2 路径不支持自动多流**,无法下发候选配置,因此也不在寻优范围内。 | ||
| 237 | + | ||
| 238 | +## 离线场景(目标机执行) | ||
| 239 | + | ||
| 240 | +编译机与执行机分离时用 `--mode offline`,驱动脚本完成整条链路: | ||
| 241 | + | ||
| 242 | +``` | ||
| 243 | +编译机 目标机 | ||
| 244 | + 逐候选 atc 编 OM(候选由 Pass 固化进模型) | ||
| 245 | + │ scp 一次性上传全部候选 OM | ||
| 246 | + ├──────────────────────────────────────────────▶ <remote_workdir>/om/ | ||
| 247 | + │ 每轮 ssh:清 plog → source CANN → 执行 run_command | ||
| 248 | + │◀────────────────────────────────────────────── <remote_workdir>/plog/ | ||
| 249 | + │ scp 回传 plog 到本轮 trial 目录 | ||
| 250 | + 解析 STEP → 排名 → 推荐(与在线口径完全一致) | ||
| 251 | + │ 结束后 rm -rf <remote_workdir> | ||
| 252 | +``` | ||
| 253 | + | ||
| 254 | +### 目标机配置 | ||
| 255 | + | ||
| 256 | +```json | ||
| 257 | +{ | ||
| 258 | + "host": "192.168.1.10", | ||
| 259 | + "port": 22, | ||
| 260 | + "user": "tester", | ||
| 261 | + "identity_file": "~/.ssh/id_rsa", | ||
| 262 | + "remote_workdir": "/home/tester/ge_ms_tune", | ||
| 263 | + "cann_env": "/usr/local/Ascend/ascend-toolkit/set_env.sh", | ||
| 264 | + "run_command": "python3 /home/tester/infer.py --om {om} --loop 20" | ||
| 265 | +} | ||
| 266 | +``` | ||
| 267 | + | ||
| 268 | +| 字段 | 必填 | 说明 | | ||
| 269 | +|---|---|---| | ||
| 270 | +| `host` / `user` | 是 | 目标机地址与登录用户 | | ||
| 271 | +| `port` | 否 | SSH 端口,默认 `22` | | ||
| 272 | +| `identity_file` | 否 | 私钥路径,配置后走密钥认证 | | ||
| 273 | +| `remote_workdir` | 是 | 目标机工作目录,存放 OM 与 plog;**必须是层级不少于两级的绝对路径,寻优结束会被整目录删除** | | ||
| 274 | +| `cann_env` | 否 | 目标机 CANN 的 `set_env.sh`,执行前 source | | ||
| 275 | +| `run_command` | 是 | 目标机上的推理命令,必须包含 `{om}` 占位符 | | ||
| 276 | + | ||
| 277 | +**认证方式**:优先用 `identity_file` 指定的密钥;未配置密钥时,从环境变量 | ||
| 278 | +`GE_MS_TARGET_PASSWORD` 读密码(需要目标机之外的编译机装有 `sshpass`,密码经 `SSHPASS` | ||
| 279 | +传递、不出现在命令行与日志里);两者都没有则使用 ssh 默认密钥。**密码不要写进 JSON。** | ||
| 280 | + | ||
| 281 | +### 目标机上的推理程序 | ||
| 282 | + | ||
| 283 | +`run_command` 指向的推理程序由你自己准备并部署到目标机(scp/rsync/镜像/CI 均可), | ||
| 284 | +驱动脚本只上传 OM,不部署程序。该程序需满足: | ||
| 285 | + | ||
| 286 | +- 加载工具传入的 `{om}`。它会被替换为目标机上该候选 OM 的绝对路径 | ||
| 287 | + (`<remote_workdir>/om/model_<候选>.om`);候选切换完全靠换 OM,程序本身无需感知多流配置; | ||
| 288 | +- 各候选使用完全相同的固定输入与迭代次数; | ||
| 289 | +- warmup 之后至少执行 `--min-steps` 次(默认 5,正式比较建议 20 起); | ||
| 290 | +- 使用打点覆盖的 ACL 接口 `aclmdlExecute`/`aclmdlExecuteV2`/`aclmdlExecuteAsync`。 | ||
| 291 | + 走 `aclmdlExecuteAsyncV2` 不产生 STEP,寻优拿不到数据;OM2 路径不支持自动多流,不能用于寻优; | ||
| 292 | +- 通过退出码反馈成败,非 0 的轮次不参与排名; | ||
| 293 | +- 不需要安装寻优 Pass,调测身份属性随 OM 携带。 | ||
| 294 | + | ||
| 295 | +它在离线侧的角色相当于在线侧的 `sample_run.py`;离线依赖真实 OM 与设备,样例不提供对应实现。 | ||
| 296 | + | ||
| 297 | +### 运行 | ||
| 298 | + | ||
| 299 | +```bash | ||
| 300 | +export GE_MS_TARGET_PASSWORD='...' # 密钥认证时不需要这行 | ||
| 301 | +python3 ge_ms_autotune.py --mode offline \ | ||
| 302 | + --compile-command "atc --model=/data/model.onnx --framework=5 \ | ||
| 303 | + --soc_version=AscendXXX --output={om_prefix}" \ | ||
| 304 | + --target target.json \ | ||
| 305 | + --strategies LoadBalance,MainStream --streams 2,4 --repeat 3 \ | ||
| 306 | + --output-dir ./tune_out_offline | ||
| 307 | +``` | ||
| 308 | + | ||
| 309 | +要点: | ||
| 310 | + | ||
| 311 | +- **不要在 `--compile-command` 里再传多流 option**,属性由 Pass 统一写入,避免两处配置打架; | ||
| 312 | +- 目标机**不需要**安装寻优 Pass,调测身份属性随 OM 携带;编译机需要; | ||
| 313 | +- 每轮执行前会清空目标机的 plog 目录,避免上一轮记录与本轮撞键; | ||
| 314 | +- `--timeout` 同时约束编译、远端执行与回传,跨机传输耗时不影响排名(排名用 STEP 里的 `cost_us`); | ||
| 315 | +- 单个候选编译失败会立即中止;某一轮远端执行失败只作废该轮,其余继续,结束仍会清理远端目录; | ||
| 316 | +- 编译机、OM 与目标机的 CANN/GE 版本和芯片型号必须匹配。 | ||
| 317 | + | ||
| 318 | +## 落地到生产 | ||
| 319 | + | ||
| 320 | +寻优结论应固化到业务侧配置,而不是继续依赖本样例的 Pass: | ||
| 321 | + | ||
| 322 | +- 在线:Session 初始化时传入 option `ge.autoMultistreamParallelMode=<推荐配置>`; | ||
| 323 | +- 离线:`atc` 编译时传入同名 option; | ||
| 324 | +- 卸载寻优 Pass(见步骤一),确保 `_auto_multistream_tuning_mode` 不再写入,关闭调测打点。 | ||
| 325 | + | ||
| 326 | +## 常见问题 | ||
| 327 | + | ||
| 328 | +| 现象 | 排查方向 | | ||
| 329 | +|---|---| | ||
| 330 | +| 所有候选都提示 `mode` 与候选不一致 | Pass 未安装、装错目录,或被 `vendors` 下其他 Pass 覆盖 | | ||
| 331 | +| 完全没有 STEP 记录 | GE 版本不含打点能力;或被测样例走了未覆盖的执行链路(`aclmdlExecuteAsyncV2`、DFlow) | | ||
| 332 | +| OM2 模型跑不出结果 | OM2 路径不支持自动多流,候选配置下发不进去,无法寻优 | | ||
| 333 | +| 提示时间区间重叠 | 被测样例并发提交多次执行,改为串行执行,或用 `--main-graph` 指定单一执行对象 | | ||
| 334 | +| 候选间耗时差异极小 | 图本身缺乏可并行分支;或算子粒度过大,多流收益被单算子耗时淹没 | | ||
| 335 | +| CV 偏大、结论不稳定 | 设备被其他业务占用、profiling 未关闭,或 `--repeat`/`--min-steps` 取值过小 | | ||
| 336 | +| 与 `ge.enableSingleStream=true` 同时配置报参数错误 | 单流与自动多流互斥,二者只能选一 | | ||
| 337 | +| offline:提示需要 `sshpass` | 编译机未装 `sshpass`;装上,或改配 `identity_file` 走密钥认证 | | ||
| 338 | +| offline:ssh 连不上或反复要密码 | 先手工 `ssh -i <key> user@host` 验证;驱动使用 `BatchMode=yes`,不会交互输密码 | | ||
| 339 | +| offline:候选编译失败 | 看 `<output-dir>/om/compile_<候选>.log`;确认 `--compile-command` 的 `{om_prefix}` 与实际产物路径一致 | | ||
| 340 | +| offline:STEP 全部缺失 | 目标机推理程序未走覆盖到的 ACL 接口,或 `cann_env` 没配导致 plog 落到别处 | | ||
| @@ -0,0 +1,372 @@ | |||
| 1 | +# GE Multi-Stream Autotune Sample | ||
| 2 | + | ||
| 3 | +[中文](README.md) | ||
| 4 | + | ||
| 5 | +GE auto multi-stream offers several stream allocation strategies (`LoadBalance`, `MainStream`, | ||
| 6 | +`WeightedLoadBalance`, `cv`). The best strategy and stream count depend on the graph structure and | ||
| 7 | +the chip, so the only reliable way to pick one is to measure. | ||
| 8 | + | ||
| 9 | +This sample provides a minimal tuning loop: **an environment variable carries the candidate | ||
| 10 | +configuration, a custom pass writes it onto the root graph, GE emits per-step timing records, and a | ||
| 11 | +driver script ranks the candidates and recommends one**. | ||
| 12 | + | ||
| 13 | +## Scope | ||
| 14 | + | ||
| 15 | +What this sample provides: | ||
| 16 | + | ||
| 17 | +- a generic custom pass that writes the candidate configuration onto the root graph, so the | ||
| 18 | + workload under test needs no code change; | ||
| 19 | +- a driver script that iterates candidates, repeats the same run, parses the STEP log, ranks | ||
| 20 | + the results and recommends a configuration; | ||
| 21 | +- two execution modes: **online** (run the command locally) and **offline** (build one OM per | ||
| 22 | + candidate, upload them to a target machine, run there, pull the plog back, parse it here); | ||
| 23 | +- a minimal sample program used to verify that the chain works end to end. | ||
| 24 | + | ||
| 25 | +What it deliberately leaves out: | ||
| 26 | + | ||
| 27 | +- no result equivalence check across candidates (compare it inside the program under test); | ||
| 28 | +- no resume after an interruption (split the matrix with `--configs` to limit the loss); | ||
| 29 | +- no CANN environment setup or device exclusivity management. | ||
| 30 | + | ||
| 31 | +## Layout | ||
| 32 | + | ||
| 33 | +``` | ||
| 34 | +multi_stream_autotune/ | ||
| 35 | +├── README.md / README_en.md | ||
| 36 | +├── ge_ms_autotune.py driver: iterate candidates, parse STEP records, rank and recommend | ||
| 37 | +├── sample_run.py minimal sample program: multi-branch static graph run in a loop | ||
| 38 | +└── custom_pass/ generic custom pass: environment variable -> root graph attribute | ||
| 39 | + ├── CMakeLists.txt | ||
| 40 | + └── src/ge_ms_autotune_pass.cpp | ||
| 41 | +``` | ||
| 42 | + | ||
| 43 | +## How it works | ||
| 44 | + | ||
| 45 | +``` | ||
| 46 | +ge_ms_autotune.py --(GE_AUTO_MULTISTREAM_PARALLEL_MODE=LoadBalance:4)--> workload process | ||
| 47 | + │ | ||
| 48 | + custom pass reads the variable at the kBeforeInferShape stage | ||
| 49 | + ▼ | ||
| 50 | + root graph attribute ge.autoMultistreamParallelMode = LoadBalance:4 | ||
| 51 | + _auto_multistream_tuning_mode = LoadBalance:4 | ||
| 52 | + │ | ||
| 53 | + ▼ | ||
| 54 | + GE allocates streams accordingly and emits a STEP record per run | ||
| 55 | +``` | ||
| 56 | + | ||
| 57 | +Key points: | ||
| 58 | + | ||
| 59 | +- **GE never reads `GE_AUTO_MULTISTREAM_PARALLEL_MODE`.** It is a contract between the sample pass | ||
| 60 | + and the driver script, so candidates can be switched without touching the workload. | ||
| 61 | +- **The graph attribute wins over the option of the same name.** When | ||
| 62 | + `ge.autoMultistreamParallelMode` is set on the graph, it overrides the Session or ATC option. | ||
| 63 | +- **`_auto_multistream_tuning_mode` is the debug identity attribute.** GE only emits STEP records | ||
| 64 | + when it is present. It is saved into the GeModel, so an OM keeps emitting records offline. | ||
| 65 | +- **Recording includes synchronization waits** (executors synchronize the actual execution stream | ||
| 66 | + to define the completion boundary), so it is for tuning only and must not be enabled in production. | ||
| 67 | +- **`default` is a candidate too.** It is the baseline without auto multi-stream, also delivered | ||
| 68 | + through the graph attribute, and is used to compute speedups. | ||
| 69 | + | ||
| 70 | +## Prerequisites | ||
| 71 | + | ||
| 72 | +- a CANN release matching the GE under test, with graph-attribute auto multi-stream and STEP | ||
| 73 | + recording available; | ||
| 74 | +- an Ascend NPU device, with profiling disabled and no other workload competing for it; | ||
| 75 | +- CMake 3.13+, a C++17 compiler and | ||
| 76 | + `$ASCEND_HOME_PATH/include/register/register_custom_pass.h` to build the pass; | ||
| 77 | +- Python 3.7+ for the driver script (standard library only). | ||
| 78 | + | ||
| 79 | +```bash | ||
| 80 | +source /path/to/cann/set_env.sh | ||
| 81 | +export ASCEND_HOME_PATH=/path/to/cann | ||
| 82 | +test -f "$ASCEND_HOME_PATH/include/register/register_custom_pass.h" | ||
| 83 | +``` | ||
| 84 | + | ||
| 85 | +## Step 1: build and install the tuning pass | ||
| 86 | + | ||
| 87 | +```bash | ||
| 88 | +cd examples/multi_stream_autotune | ||
| 89 | +cmake -S custom_pass -B build -DASCEND_HOME_PATH="${ASCEND_HOME_PATH}" | ||
| 90 | +cmake --build build --parallel | ||
| 91 | + | ||
| 92 | +# install into the directory GE scans for custom passes (the vendor name is up to you) | ||
| 93 | +PASS_DIR="${ASCEND_OPP_PATH:-$ASCEND_HOME_PATH/opp}/vendors/ge_ms_autotune/custom_fusion_passes" | ||
| 94 | +mkdir -p "${PASS_DIR}" | ||
| 95 | +install -m 750 build/libge_ms_autotune_pass.so "${PASS_DIR}/" | ||
| 96 | +``` | ||
| 97 | + | ||
| 98 | +Notes: | ||
| 99 | + | ||
| 100 | +- move away any other pass in that directory that writes multi-stream graph attributes, otherwise | ||
| 101 | + the attributes overwrite each other; | ||
| 102 | +- **remove the library once tuning is done**, so debug recording never reaches production: | ||
| 103 | + `rm -f "${PASS_DIR}/libge_ms_autotune_pass.so"`. | ||
| 104 | + | ||
| 105 | +## Step 2: prepare the program under test | ||
| 106 | + | ||
| 107 | +The program under test (the workload whose cost is compared across candidates) can be any | ||
| 108 | +command that compiles the graph in-process and executes it repeatedly, as long as it: | ||
| 109 | + | ||
| 110 | +- rebuilds the graph on every process start and never reuses a graph or model cache left by the | ||
| 111 | + previous candidate; | ||
| 112 | +- uses exactly the same inputs and iteration count for every candidate, and runs at least | ||
| 113 | + `--min-steps` times after warmup; | ||
| 114 | +- reports failure through its exit code (a non-zero run is excluded from the ranking). | ||
| 115 | + | ||
| 116 | +`sample_run.py` is a reference implementation: it builds a static graph of four independent | ||
| 117 | +pointwise branches (which can be dispatched to different streams) and runs `--steps` iterations | ||
| 118 | +after one warmup step: | ||
| 119 | + | ||
| 120 | +```bash | ||
| 121 | +python3 sample_run.py --steps 12 --dim 512 | ||
| 122 | +``` | ||
| 123 | + | ||
| 124 | +## Step 3: run the tuning | ||
| 125 | + | ||
| 126 | +```bash | ||
| 127 | +python3 ge_ms_autotune.py \ | ||
| 128 | + --run-command "python3 sample_run.py --steps 12" \ | ||
| 129 | + --strategies LoadBalance,MainStream \ | ||
| 130 | + --streams 2,4,8 \ | ||
| 131 | + --repeat 3 \ | ||
| 132 | + --output-dir ./tune_out | ||
| 133 | +``` | ||
| 134 | + | ||
| 135 | +For every candidate the driver injects `GE_AUTO_MULTISTREAM_PARALLEL_MODE` and a per-run | ||
| 136 | +`ASCEND_PROCESS_LOG_PATH`, executes the command, collects STEP records from stdout and from that | ||
| 137 | +run's plog directory, then validates and aggregates them. The console output looks like: | ||
| 138 | + | ||
| 139 | +``` | ||
| 140 | +候选配置(7 个 × 3 轮):default, LoadBalance:2, LoadBalance:4, ... | ||
| 141 | + | ||
| 142 | +[000] 配置=default 第 1 轮:python3 sample_run.py --steps 12 | ||
| 143 | + 退出码=0 STEP=13 有效=是 耗时=21.4s | ||
| 144 | +... | ||
| 145 | + | ||
| 146 | +寻优结果(按中位耗时升序): | ||
| 147 | +配置 有效轮次 步数 平均(ms) 中位(ms) P90(ms) CV 加速比 结论 | ||
| 148 | +LoadBalance:4 3/3 36 12.104 12.088 12.301 0.014 1.243 提升 | ||
| 149 | +MainStream:4 3/3 36 13.552 13.489 13.702 0.011 1.114 提升 | ||
| 150 | +default 3/3 36 15.037 15.028 15.311 0.009 1.000 持平 | ||
| 151 | + | ||
| 152 | +[结论] 推荐配置:LoadBalance:4,相对 default 加速比 1.243,中位耗时 12.088 ms。 | ||
| 153 | +[复现] GE_AUTO_MULTISTREAM_PARALLEL_MODE=LoadBalance:4 python3 sample_run.py --steps 12 | ||
| 154 | +``` | ||
| 155 | + | ||
| 156 | +To refine the stream count around the winner, run the neighbouring values again: | ||
| 157 | + | ||
| 158 | +```bash | ||
| 159 | +python3 ge_ms_autotune.py --run-command "..." \ | ||
| 160 | + --configs default,LoadBalance:3,LoadBalance:4,LoadBalance:5 --output-dir ./tune_out_stage2 | ||
| 161 | +``` | ||
| 162 | + | ||
| 163 | +## Options | ||
| 164 | + | ||
| 165 | +| Option | Default | Description | | ||
| 166 | +|---|---|---| | ||
| 167 | +| `--mode` | `online` | `online` runs locally; `offline` builds OMs and runs them on a target machine | | ||
| 168 | +| `--run-command` | required (online) | Command under test, quoted as a whole; split with shell lexing and executed directly, not through a shell | | ||
| 169 | +| `--compile-command` | required (offline) | OM build command; use `{om}` (with `.om`) or `{om_prefix}` (without suffix) for the output path | | ||
| 170 | +| `--target` | required (offline) | Path to the target machine JSON, see [Offline mode](#offline-mode-target-machine) | | ||
| 171 | +| `--om-dir` | `<output-dir>/om` | offline: where OMs and build logs are stored | | ||
| 172 | +| `--strategies` | `LoadBalance,MainStream` | Candidate strategies: `LoadBalance`, `MainStream`, `WeightedLoadBalance`, `cv` | | ||
| 173 | +| `--streams` | `2,4,8` | Candidate stream counts in `[1,64]`; the `cv` strategy takes no stream count | | ||
| 174 | +| `--configs` | empty | Explicit candidate list (for example `default,LoadBalance:4`); overrides the two matrix options above | | ||
| 175 | +| `--repeat` | `3` | Runs per candidate; use at least 3 for a real comparison | | ||
| 176 | +| `--drop-first` | `1` | Drop this many leading STEP records (warmup) | | ||
| 177 | +| `--min-steps` | `5` | Minimum valid STEP records per run | | ||
| 178 | +| `--main-graph` | auto | Pick the main execution object explicitly: `session_id:graph_id` or `model:model_id`; by default the one with the most records | | ||
| 179 | +| `--timeout` | `1800` | Per-run timeout in seconds, `0` disables it | | ||
| 180 | +| `--output-dir` | `./ge_ms_autotune_output` | Result directory, must be missing or empty | | ||
| 181 | + | ||
| 182 | +The `default` baseline is always added as the first candidate. | ||
| 183 | + | ||
| 184 | +## Output and how to read it | ||
| 185 | + | ||
| 186 | +``` | ||
| 187 | +tune_out/ | ||
| 188 | +├── summary.csv / summary.json per-candidate summary, per-run details and reject reasons | ||
| 189 | +├── om/ offline only: one OM per candidate plus build logs | ||
| 190 | +├── target_*.log offline only: remote prepare, upload and cleanup logs | ||
| 191 | +└── trial_000_default_r1/ | ||
| 192 | + ├── stdout.log stdout and stderr of the run (the ssh session when offline) | ||
| 193 | + ├── steps.csv all STEP records parsed from this run | ||
| 194 | + ├── fetch_plog.log offline only: plog transfer log | ||
| 195 | + └── plog/ GE logs of this run (a copy pulled back when offline) | ||
| 196 | +``` | ||
| 197 | + | ||
| 198 | +Statistics and recommendation rules: | ||
| 199 | + | ||
| 200 | +- per candidate, the step costs of the main execution object are pooled across all valid runs, and | ||
| 201 | + candidates are ranked by **median cost**; | ||
| 202 | +- speedup = `median of default / median of candidate`; | ||
| 203 | +- `>=1.05` counts as an improvement, `[0.98, 1.05)` as neutral, `<0.98` as a regression; | ||
| 204 | +- the candidate with the smallest median and a speedup of `>=1.05` is recommended; if none | ||
| 205 | + qualifies, keeping `default` is recommended; | ||
| 206 | +- `CV` only indicates how stable the samples are and never affects the ranking; above `0.05` the | ||
| 207 | + driver suggests increasing `--repeat`. | ||
| 208 | + | ||
| 209 | +## Validity gates | ||
| 210 | + | ||
| 211 | +A run that trips any of these is excluded from the ranking, with the reason printed on the console | ||
| 212 | +and stored in `summary.json`: | ||
| 213 | + | ||
| 214 | +| Check | Meaning | | ||
| 215 | +|---|---| | ||
| 216 | +| Non-zero exit code | The command failed or timed out | | ||
| 217 | +| Malformed log | Missing fields, non-integer values, `cost_us` inconsistent with the interval, missing or mixed execution identity | | ||
| 218 | +| `mode` mismatch | The `mode` in the STEP record differs from the current candidate, usually because the pass is not installed or is shadowed by another pass | | ||
| 219 | +| `ret`/`sync_ret` non-zero | The execution or synchronization interface returned a failure | | ||
| 220 | +| Too few steps | Fewer than `--min-steps` records remain after dropping warmup | | ||
| 221 | +| Overlapping intervals | STEP intervals of the main object overlap, so they cannot be treated as serial costs | | ||
| 222 | + | ||
| 223 | +> Result equivalence across candidates is out of scope; verify it inside the workload when needed | ||
| 224 | +> (for example by fixing the inputs and comparing an output digest). | ||
| 225 | + | ||
| 226 | +## STEP record format | ||
| 227 | + | ||
| 228 | +Records are emitted from inside the executors, so online and offline share the same instrumentation | ||
| 229 | +points and both identify the execution object by `model_id`: | ||
| 230 | + | ||
| 231 | +``` | ||
| 232 | +[EVENT] GE(pid,proc): [GE_MS_TUNE][STEP] api=NnExecute mode=LoadBalance:4 \ | ||
| 233 | + model_id=7 step=3 start_us=100 end_us=140 cost_us=40 sync_us=0 ret=0 sync_ret=0 | ||
| 234 | +``` | ||
| 235 | + | ||
| 236 | +| Field | Meaning | | ||
| 237 | +|---|---| | ||
| 238 | +| `api` | Instrumentation site: `NnExecute`/`Run` (static shape), `ModelV2Executor` (RT2.0 dynamic shape) | | ||
| 239 | +| `mode` | Multi-stream configuration in effect, used to confirm the candidate was really applied | | ||
| 240 | +| `session_id`+`graph_id` / `model_id` | Execution object identity, one of the two; the executors currently emit `model_id` | | ||
| 241 | +| `step` | Step index, starting at 0 | | ||
| 242 | +| `start_us`/`end_us`/`cost_us` | Start, end and cost in microseconds, `cost_us = end_us - start_us` | | ||
| 243 | +| `sync_us` | Synchronization wait inside the cost, in microseconds | | ||
| 244 | +| `ret`/`sync_ret` | Execution and synchronization return values, 0 means success | | ||
| 245 | + | ||
| 246 | +The measured window is "task submission -> stream synchronization done"; it excludes H2D/D2H copies | ||
| 247 | +and API-layer overhead, so the numbers are smaller than the end-to-end per-step latency. | ||
| 248 | + | ||
| 249 | +Covered: the static-shape stack (`DavinciModel`, including its queue-async worker) and the RT2.0 | ||
| 250 | +dynamic-shape stack (`ModelV2Executor`), for both online and offline execution. | ||
| 251 | +Not covered: `aclmdlExecuteAsyncV2` and the DFlow execution path. | ||
| 252 | + | ||
| 253 | +The sunset RT1.0 dynamic-shape executor (`HybridModelRtV1Executor`) and the RtV2Pipeline executor | ||
| 254 | +emit no records, or records whose numbers cannot be trusted. | ||
| 255 | +**The OM2 path does not support auto multi-stream**, so no candidate can be applied to it and it | ||
| 256 | +is out of scope for tuning. | ||
| 257 | + | ||
| 258 | +## Offline mode (target machine) | ||
| 259 | + | ||
| 260 | +When the build machine and the execution machine differ, use `--mode offline` and the driver | ||
| 261 | +handles the whole chain. The key idea is that **the candidate configuration is baked into the OM | ||
| 262 | +at build time**, so the target machine only runs the OM and produces logs. | ||
| 263 | + | ||
| 264 | +``` | ||
| 265 | +build machine target machine | ||
| 266 | + one atc build per candidate (pass bakes the attribute) | ||
| 267 | + │ scp: upload every candidate OM once | ||
| 268 | + ├──────────────────────────────────────────────▶ <remote_workdir>/om/ | ||
| 269 | + │ per run, over ssh: clear plog -> source CANN -> run_command | ||
| 270 | + │◀────────────────────────────────────────────── <remote_workdir>/plog/ | ||
| 271 | + │ scp: pull the plog into this run's trial directory | ||
| 272 | + parse STEP -> rank -> recommend (identical rules to online) | ||
| 273 | + │ finally: rm -rf <remote_workdir> | ||
| 274 | +``` | ||
| 275 | + | ||
| 276 | +### Target machine configuration | ||
| 277 | + | ||
| 278 | +```json | ||
| 279 | +{ | ||
| 280 | + "host": "192.168.1.10", | ||
| 281 | + "port": 22, | ||
| 282 | + "user": "tester", | ||
| 283 | + "identity_file": "~/.ssh/id_rsa", | ||
| 284 | + "remote_workdir": "/home/tester/ge_ms_tune", | ||
| 285 | + "cann_env": "/usr/local/Ascend/ascend-toolkit/set_env.sh", | ||
| 286 | + "run_command": "python3 /home/tester/infer.py --om {om} --loop 20" | ||
| 287 | +} | ||
| 288 | +``` | ||
| 289 | + | ||
| 290 | +| Field | Required | Meaning | | ||
| 291 | +|---|---|---| | ||
| 292 | +| `host` / `user` | yes | Target address and login user | | ||
| 293 | +| `port` | no | SSH port, `22` by default | | ||
| 294 | +| `identity_file` | no | Private key path; key authentication is used when set | | ||
| 295 | +| `remote_workdir` | yes | Working directory holding OMs and plog; **must be an absolute path of at least two levels, and is deleted entirely when tuning ends** | | ||
| 296 | +| `cann_env` | no | `set_env.sh` of the CANN on the target, sourced before each run | | ||
| 297 | +| `run_command` | yes | Inference command on the target, must contain the `{om}` placeholder | | ||
| 298 | + | ||
| 299 | +**Authentication**: the key in `identity_file` wins; without it the password is read from the | ||
| 300 | +`GE_MS_TARGET_PASSWORD` environment variable (this needs `sshpass` on the build machine; the | ||
| 301 | +password travels through `SSHPASS` and never appears on a command line or in a log); with neither, | ||
| 302 | +the default ssh keys are used. **Never put the password in the JSON.** | ||
| 303 | + | ||
| 304 | +### The inference program on the target | ||
| 305 | + | ||
| 306 | +The program behind `run_command` is yours to write and to deploy on the target (scp, rsync, an | ||
| 307 | +image, CI — whatever you use); the driver uploads OMs only, never the program. It must: | ||
| 308 | + | ||
| 309 | +- load the `{om}` it is given — the placeholder becomes the absolute path of that candidate's OM | ||
| 310 | + on the target (`<remote_workdir>/om/model_<config>.om`). Candidates are switched purely by | ||
| 311 | + swapping the OM, so the program itself needs no multi-stream awareness; | ||
| 312 | +- use exactly the same fixed inputs and iteration count for every candidate; | ||
| 313 | +- run at least `--min-steps` iterations after warmup (5 by default, 20+ for a real comparison); | ||
| 314 | +- use a recorded ACL interface: `aclmdlExecute`, `aclmdlExecuteV2` or `aclmdlExecuteAsync`. | ||
| 315 | + `aclmdlExecuteAsyncV2` emits no STEP record; the OM2 path does not support auto multi-stream at | ||
| 316 | + all and cannot be tuned; | ||
| 317 | +- report failure through its exit code — a non-zero run is excluded from the ranking; | ||
| 318 | +- it does **not** need the tuning pass installed, since the debug identity travels with the OM. | ||
| 319 | + | ||
| 320 | +It plays the role that `sample_run.py` plays online; offline needs a real OM and a real device, so | ||
| 321 | +no equivalent sample ships with this directory. | ||
| 322 | + | ||
| 323 | +### Running | ||
| 324 | + | ||
| 325 | +```bash | ||
| 326 | +export GE_MS_TARGET_PASSWORD='...' # not needed with key authentication | ||
| 327 | +python3 ge_ms_autotune.py --mode offline \ | ||
| 328 | + --compile-command "atc --model=/data/model.onnx --framework=5 \ | ||
| 329 | + --soc_version=AscendXXX --output={om_prefix}" \ | ||
| 330 | + --target target.json \ | ||
| 331 | + --strategies LoadBalance,MainStream --streams 2,4 --repeat 3 \ | ||
| 332 | + --output-dir ./tune_out_offline | ||
| 333 | +``` | ||
| 334 | + | ||
| 335 | +Notes: | ||
| 336 | + | ||
| 337 | +- **Do not pass a multi-stream option to `--compile-command`**: the pass writes the attribute, and | ||
| 338 | + two sources of truth would conflict; | ||
| 339 | +- the pass does **not** need to be installed on the target, since the debug identity travels with | ||
| 340 | + the OM; the build machine does need it; | ||
| 341 | +- the target plog directory is cleared before every run, so records of the previous run cannot | ||
| 342 | + collide with the current one; | ||
| 343 | +- `--timeout` bounds the build, the remote run and the transfer alike; transfer time never affects | ||
| 344 | + the ranking, which uses `cost_us` from the STEP records; | ||
| 345 | +- a failing build aborts immediately; a failing remote run only voids that run, the rest continue, | ||
| 346 | + and the remote directory is still cleaned up at the end; | ||
| 347 | +- the CANN/GE version and chip model must match across the build machine, the OM and the target. | ||
| 348 | + | ||
| 349 | +## Moving the result into production | ||
| 350 | + | ||
| 351 | +Pin the tuning result on the business side instead of keeping the sample pass around: | ||
| 352 | + | ||
| 353 | +- online: pass the option `ge.autoMultistreamParallelMode=<config>` when initializing the Session; | ||
| 354 | +- offline: pass the same option to `atc`; | ||
| 355 | +- uninstall the tuning pass (see step 1) so `_auto_multistream_tuning_mode` is no longer written | ||
| 356 | + and debug recording stays off. | ||
| 357 | + | ||
| 358 | +## Troubleshooting | ||
| 359 | + | ||
| 360 | +| Symptom | Where to look | | ||
| 361 | +|---|---| | ||
| 362 | +| Every candidate reports a `mode` mismatch | The pass is missing, installed in the wrong directory, or shadowed by another pass under `vendors` | | ||
| 363 | +| No STEP record at all | The GE build has no recording support, or the workload uses an uncovered path (`aclmdlExecuteAsyncV2`, DFlow) | | ||
| 364 | +| An OM2 model produces no result | The OM2 path does not support auto multi-stream, so no candidate can be applied and tuning is impossible | | ||
| 365 | +| Overlapping intervals reported | The workload submits runs concurrently; serialize it or select a single object with `--main-graph` | | ||
| 366 | +| Nearly identical costs across candidates | The graph has no parallel branches, or single-operator cost dominates the multi-stream gain | | ||
| 367 | +| Large CV and unstable conclusions | The device is shared, profiling is still on, or `--repeat`/`--min-steps` are too small | | ||
| 368 | +| Parameter error together with `ge.enableSingleStream=true` | Single stream and auto multi-stream are mutually exclusive | | ||
| 369 | +| offline: `sshpass` is reported as missing | Install it on the build machine, or switch to key authentication with `identity_file` | | ||
| 370 | +| offline: ssh cannot connect or keeps asking for a password | Verify `ssh -i <key> user@host` by hand first; the driver uses `BatchMode=yes` and never prompts | | ||
| 371 | +| offline: a candidate fails to build | Read `<output-dir>/om/compile_<config>.log` and check that `{om_prefix}` matches the real output path | | ||
| 372 | +| offline: no STEP record at all | The target program uses an uncovered ACL path, or `cann_env` is unset so the plog lands elsewhere | | ||
| @@ -0,0 +1,34 @@ | |||
| 1 | +cmake_minimum_required(VERSION 3.5.1) | ||
| 2 | +project(ge_ms_autotune_pass) | ||
| 3 | + | ||
| 4 | +set(ASCEND_HOME_PATH "" CACHE PATH "CANN installation root") | ||
| 5 | + | ||
| 6 | +if (NOT ASCEND_HOME_PATH) | ||
| 7 | + if (DEFINED ENV{ASCEND_HOME_PATH}) | ||
| 8 | + set(ASCEND_HOME_PATH $ENV{ASCEND_HOME_PATH}) | ||
| 9 | + else () | ||
| 10 | + message(FATAL_ERROR "ASCEND_HOME_PATH is required") | ||
| 11 | + endif () | ||
| 12 | +endif () | ||
| 13 | + | ||
| 14 | +add_library(${PROJECT_NAME} SHARED src/ge_ms_autotune_pass.cpp) | ||
| 15 | +target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17) | ||
| 16 | +target_compile_options(${PROJECT_NAME} PRIVATE -g -Wall) | ||
| 17 | +target_compile_definitions(${PROJECT_NAME} PRIVATE _GLIBCXX_USE_CXX11_ABI=0) | ||
| 18 | +target_include_directories(${PROJECT_NAME} PRIVATE | ||
| 19 | + ${ASCEND_HOME_PATH}/include/graph | ||
| 20 | + ${ASCEND_HOME_PATH}/include/ge | ||
| 21 | + ${ASCEND_HOME_PATH}/include/register | ||
| 22 | + ${ASCEND_HOME_PATH}/include | ||
| 23 | +) | ||
| 24 | +target_link_directories(${PROJECT_NAME} PRIVATE | ||
| 25 | + ${ASCEND_HOME_PATH}/lib64/stub | ||
| 26 | + ${ASCEND_HOME_PATH}/lib64 | ||
| 27 | +) | ||
| 28 | +target_link_libraries(${PROJECT_NAME} PRIVATE | ||
| 29 | + -Wl,--no-as-needed | ||
| 30 | + graph | ||
| 31 | + register | ||
| 32 | + ge_compiler | ||
| 33 | + -Wl,--as-needed | ||
| 34 | +) | ||
| @@ -0,0 +1,51 @@ | |||
| 1 | +/** | ||
| 2 | + * Copyright (c) 2026 Huawei Technologies Co., Ltd. | ||
| 3 | + * This program is free software; you can redistribute it and/or modify it under the terms and conditions of | ||
| 4 | + * CANN Open Software License Agreement Version 2.0 (the "License"). | ||
| 5 | + * Please refer to the License for details. You may not use this file except in compliance with the License. | ||
| 6 | + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, | ||
| 7 | + * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. | ||
| 8 | + * See LICENSE in the root of the software repository for the full text of the License. | ||
| 9 | + */ | ||
| 10 | + | ||
| 11 | + | ||
| 12 | + | ||
| 13 | + | ||
| 14 | + | ||
| 15 | + | ||
| 16 | +namespace { | ||
| 17 | +constexpr const char *const kAutoMultistreamParallelModeAttr = "ge.autoMultistreamParallelMode"; | ||
| 18 | +constexpr const char *const kAutoMultistreamTuningModeAttr = "_auto_multistream_tuning_mode"; | ||
| 19 | +constexpr const char *const kAutoMultistreamModeEnv = "GE_AUTO_MULTISTREAM_PARALLEL_MODE"; | ||
| 20 | + | ||
| 21 | +ge::graphStatus SetStringAttr(const ge::GraphPtr &graph, const char *const name, const char *const value, | ||
| 22 | + ge::CustomPassContext &context) { | ||
| 23 | + ge::AttrValue attr_value; | ||
| 24 | + if (attr_value.SetAttrValue(ge::AscendString(value)) != ge::GRAPH_SUCCESS) { | ||
| 25 | + context.SetErrorMessage(ge::AscendString("Failed to create auto multistream graph attribute.")); | ||
| 26 | + return ge::GRAPH_FAILED; | ||
| 27 | + } | ||
| 28 | + if (graph->SetAttr(ge::AscendString(name), attr_value) != ge::GRAPH_SUCCESS) { | ||
| 29 | + context.SetErrorMessage(ge::AscendString("Failed to set auto multistream graph attribute.")); | ||
| 30 | + return ge::GRAPH_FAILED; | ||
| 31 | + } | ||
| 32 | + return ge::GRAPH_SUCCESS; | ||
| 33 | +} | ||
| 34 | + | ||
| 35 | +ge::graphStatus GeMsAutotunePass(ge::GraphPtr &graph, ge::CustomPassContext &context) { | ||
| 36 | + if (graph == nullptr) { | ||
| 37 | + context.SetErrorMessage(ge::AscendString("Graph is null.")); | ||
| 38 | + return ge::GRAPH_FAILED; | ||
| 39 | + } | ||
| 40 | + const char *const mode = std::getenv(kAutoMultistreamModeEnv); | ||
| 41 | + if ((mode == nullptr) || (mode[0] == '\0')) { | ||
| 42 | + return ge::GRAPH_SUCCESS; | ||
| 43 | + } | ||
| 44 | + if (SetStringAttr(graph, kAutoMultistreamParallelModeAttr, mode, context) != ge::GRAPH_SUCCESS) { | ||
| 45 | + return ge::GRAPH_FAILED; | ||
| 46 | + } | ||
| 47 | + return SetStringAttr(graph, kAutoMultistreamTuningModeAttr, mode, context); | ||
| 48 | +} | ||
| 49 | +} // namespace | ||
| 50 | + | ||
| 51 | +REGISTER_CUSTOM_PASS("GeMsAutotunePass").CustomPassFn(GeMsAutotunePass).Stage(ge::CustomPassStage::kBeforeInferShape); | ||
| @@ -0,0 +1,1107 @@ | |||
| 1 | +#!/usr/bin/env python3 | ||
| 2 | +# ---------------------------------------------------------------------------- | ||
| 3 | +# Copyright (c) 2026 Huawei Technologies Co., Ltd. | ||
| 4 | +# This program is free software, you can redistribute it and/or modify it under the terms and conditions of | ||
| 5 | +# CANN Open Software License Agreement Version 2.0 (the "License"). | ||
| 6 | +# Please refer to the License for details. You may not use this file except in compliance with the License. | ||
| 7 | +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, | ||
| 8 | +# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. | ||
| 9 | +# See LICENSE in the root of the software repository for the full text of the License. | ||
| 10 | +# ---------------------------------------------------------------------------- | ||
| 11 | + | ||
| 12 | +"""GE 多流自动寻优驱动。 | ||
| 13 | + | ||
| 14 | +对每个候选多流配置反复执行同一条用户命令,从 `[GE_MS_TUNE][STEP]` 事件日志中提取 | ||
| 15 | +单步执行耗时,统计后给出候选排名与推荐配置。 | ||
| 16 | + | ||
| 17 | +候选配置通过环境变量 GE_AUTO_MULTISTREAM_PARALLEL_MODE 下发给被测进程,由 | ||
| 18 | +custom_pass/ 下的自定义 Pass 在编图阶段写入根图属性;GE 不直接读取该环境变量。 | ||
| 19 | +使用前需先按 README 编译并安装该 Pass。 | ||
| 20 | +""" | ||
| 21 | + | ||
| 22 | +import argparse | ||
| 23 | +import csv | ||
| 24 | +import json | ||
| 25 | +import math | ||
| 26 | +import os | ||
| 27 | +import re | ||
| 28 | +import shlex | ||
| 29 | +import shutil | ||
| 30 | +import statistics | ||
| 31 | +import subprocess | ||
| 32 | +import sys | ||
| 33 | +import time | ||
| 34 | +import unicodedata | ||
| 35 | +from dataclasses import dataclass, field | ||
| 36 | +from pathlib import Path | ||
| 37 | +from typing import Dict, List, Optional, Sequence, Tuple | ||
| 38 | + | ||
| 39 | +STEP_TAG = "[GE_MS_TUNE][STEP]" | ||
| 40 | +MODE_ENV = "GE_AUTO_MULTISTREAM_PARALLEL_MODE" | ||
| 41 | +PASSWORD_ENV = "GE_MS_TARGET_PASSWORD" | ||
| 42 | +STRATEGIES = ("LoadBalance", "MainStream", "WeightedLoadBalance", "cv") | ||
| 43 | +BASELINE_CONFIG = "default" | ||
| 44 | +MAX_STREAMS = 64 | ||
| 45 | +REQUIRED_FIELDS = ( | ||
| 46 | + "api", | ||
| 47 | + "mode", | ||
| 48 | + "step", | ||
| 49 | + "start_us", | ||
| 50 | + "end_us", | ||
| 51 | + "cost_us", | ||
| 52 | + "sync_us", | ||
| 53 | + "ret", | ||
| 54 | + "sync_ret", | ||
| 55 | +) | ||
| 56 | +NUMERIC_FIELDS = tuple(name for name in REQUIRED_FIELDS if name not in ("api", "mode")) | ||
| 57 | +POSITIVE_SPEEDUP = 1.05 | ||
| 58 | +NEUTRAL_SPEEDUP = 0.98 | ||
| 59 | + | ||
| 60 | +# 执行对象标识:在线为 ("graph", session_id, graph_id),离线为 ("model", model_id, 0)。 | ||
| 61 | +ExecutionKey = Tuple[str, int, int] | ||
| 62 | + | ||
| 63 | + | ||
| 64 | +class AutotuneError(RuntimeError): | ||
| 65 | + """参数或环境不合法,无法继续寻优。""" | ||
| 66 | + | ||
| 67 | + | ||
| 68 | + | ||
| 69 | +class StepRecord: | ||
| 70 | + api: str | ||
| 71 | + mode: str | ||
| 72 | + key: ExecutionKey | ||
| 73 | + step: int | ||
| 74 | + start_us: int | ||
| 75 | + end_us: int | ||
| 76 | + cost_us: int | ||
| 77 | + sync_us: int | ||
| 78 | + ret: int | ||
| 79 | + sync_ret: int | ||
| 80 | + | ||
| 81 | + | ||
| 82 | + | ||
| 83 | +class TrialResult: | ||
| 84 | + config: str | ||
| 85 | + repeat: int | ||
| 86 | + exit_code: Optional[int] | ||
| 87 | + wall_seconds: float | ||
| 88 | + step_count: int | ||
| 89 | + main_key: Optional[ExecutionKey] | ||
| 90 | + costs: List[int] = field(default_factory=list) | ||
| 91 | + reasons: List[str] = field(default_factory=list) | ||
| 92 | + | ||
| 93 | + | ||
| 94 | + def valid(self) -> bool: | ||
| 95 | + return not self.reasons | ||
| 96 | + | ||
| 97 | + | ||
| 98 | + | ||
| 99 | +class Target: | ||
| 100 | + """离线执行机(目标机)连接信息,来自 --target 指向的 JSON。""" | ||
| 101 | + | ||
| 102 | + host: str | ||
| 103 | + user: str | ||
| 104 | + remote_workdir: str | ||
| 105 | + run_command: str | ||
| 106 | + port: int = 22 | ||
| 107 | + identity_file: Optional[Path] = None | ||
| 108 | + cann_env: Optional[str] = None | ||
| 109 | + password: Optional[str] = None | ||
| 110 | + | ||
| 111 | + | ||
| 112 | + def destination(self) -> str: | ||
| 113 | + return "{}@{}".format(self.user, self.host) | ||
| 114 | + | ||
| 115 | + | ||
| 116 | + | ||
| 117 | +class ConfigSummary: | ||
| 118 | + config: str | ||
| 119 | + trials: int | ||
| 120 | + valid_trials: int | ||
| 121 | + steps: int | ||
| 122 | + mean_us: Optional[float] = None | ||
| 123 | + median_us: Optional[float] = None | ||
| 124 | + p90_us: Optional[float] = None | ||
| 125 | + cv: Optional[float] = None | ||
| 126 | + speedup: Optional[float] = None | ||
| 127 | + reasons: List[str] = field(default_factory=list) | ||
| 128 | + | ||
| 129 | + | ||
| 130 | +# ---------------------------------------------------------------- 候选配置 | ||
| 131 | + | ||
| 132 | + | ||
| 133 | +def validate_config(config: str) -> None: | ||
| 134 | + if config in (BASELINE_CONFIG, "cv"): | ||
| 135 | + return | ||
| 136 | + if config.count(":") != 1: | ||
| 137 | + raise AutotuneError( | ||
| 138 | + "非法配置 {!r}:格式应为“策略名:流数”,例如 LoadBalance:2。".format(config) | ||
| 139 | + ) | ||
| 140 | + strategy, streams = config.split(":", 1) | ||
| 141 | + if strategy not in STRATEGIES or strategy == "cv": | ||
| 142 | + raise AutotuneError( | ||
| 143 | + "未知策略 {!r},可选:{}。".format(strategy, "/".join(STRATEGIES)) | ||
| 144 | + ) | ||
| 145 | + if not streams.isdigit() or not 1 <= int(streams) <= MAX_STREAMS: | ||
| 146 | + raise AutotuneError( | ||
| 147 | + "配置 {!r} 的流数必须是 [1,{}] 的十进制整数。".format(config, MAX_STREAMS) | ||
| 148 | + ) | ||
| 149 | + | ||
| 150 | + | ||
| 151 | +def split_csv(value: str) -> List[str]: | ||
| 152 | + items: List[str] = [] | ||
| 153 | + for item in value.split(","): | ||
| 154 | + item = item.strip() | ||
| 155 | + if item and item not in items: | ||
| 156 | + items.append(item) | ||
| 157 | + return items | ||
| 158 | + | ||
| 159 | + | ||
| 160 | +def build_configs( | ||
| 161 | + configs_arg: Optional[str], strategies_arg: str, streams_arg: str | ||
| 162 | +) -> List[str]: | ||
| 163 | + """构造候选列表,基准配置 default 始终排在首位。""" | ||
| 164 | + if configs_arg: | ||
| 165 | + configs = split_csv(configs_arg) | ||
| 166 | + if not configs: | ||
| 167 | + raise AutotuneError("--configs 不能为空。") | ||
| 168 | + else: | ||
| 169 | + configs = expand_matrix(split_csv(strategies_arg), split_csv(streams_arg)) | ||
| 170 | + for config in configs: | ||
| 171 | + validate_config(config) | ||
| 172 | + if BASELINE_CONFIG in configs: | ||
| 173 | + configs.remove(BASELINE_CONFIG) | ||
| 174 | + return [BASELINE_CONFIG] + configs | ||
| 175 | + | ||
| 176 | + | ||
| 177 | +def expand_matrix(strategies: Sequence[str], streams: Sequence[str]) -> List[str]: | ||
| 178 | + if not strategies: | ||
| 179 | + raise AutotuneError("--strategies 不能为空。") | ||
| 180 | + unknown = [item for item in strategies if item not in STRATEGIES] | ||
| 181 | + if unknown: | ||
| 182 | + raise AutotuneError("未知策略:{}。".format(",".join(unknown))) | ||
| 183 | + if not streams: | ||
| 184 | + raise AutotuneError("--streams 不能为空。") | ||
| 185 | + configs: List[str] = [] | ||
| 186 | + for strategy in strategies: | ||
| 187 | + if strategy == "cv": | ||
| 188 | + configs.append("cv") | ||
| 189 | + continue | ||
| 190 | + configs.extend("{}:{}".format(strategy, item) for item in streams) | ||
| 191 | + return configs | ||
| 192 | + | ||
| 193 | + | ||
| 194 | +# ---------------------------------------------------------------- STEP 日志解析 | ||
| 195 | + | ||
| 196 | + | ||
| 197 | +def parse_step_line(line: str) -> Tuple[Optional[StepRecord], Optional[str]]: | ||
| 198 | + """解析一行 STEP 日志,返回记录或错误描述;非 STEP 行返回 (None, None)。""" | ||
| 199 | + position = line.find(STEP_TAG) | ||
| 200 | + if position < 0: | ||
| 201 | + return (None, None) | ||
| 202 | + fields: Dict[str, str] = {} | ||
| 203 | + for token in line[position + len(STEP_TAG) :].split(): | ||
| 204 | + if "=" not in token: | ||
| 205 | + break | ||
| 206 | + name, value = token.split("=", 1) | ||
| 207 | + if name and value: | ||
| 208 | + fields[name] = value | ||
| 209 | + missing = [name for name in REQUIRED_FIELDS if name not in fields] | ||
| 210 | + if missing: | ||
| 211 | + return (None, "缺少字段 {}".format(",".join(missing))) | ||
| 212 | + key, error = execution_key(fields) | ||
| 213 | + if error is not None: | ||
| 214 | + return (None, error) | ||
| 215 | + return build_step_record(fields, key) | ||
| 216 | + | ||
| 217 | + | ||
| 218 | +def execution_key( | ||
| 219 | + fields: Dict[str, str], | ||
| 220 | +) -> Tuple[Optional[ExecutionKey], Optional[str]]: | ||
| 221 | + has_graph = all(name in fields for name in ("session_id", "graph_id")) | ||
| 222 | + has_model = "model_id" in fields | ||
| 223 | + if has_graph == has_model: | ||
| 224 | + return (None, "必须且只能包含 session_id+graph_id 或 model_id 一种执行身份") | ||
| 225 | + try: | ||
| 226 | + if has_graph: | ||
| 227 | + return (("graph", int(fields["session_id"]), int(fields["graph_id"])), None) | ||
| 228 | + return (("model", int(fields["model_id"]), 0), None) | ||
| 229 | + except ValueError: | ||
| 230 | + return (None, "执行身份字段不是整数") | ||
| 231 | + | ||
| 232 | + | ||
| 233 | +def build_step_record( | ||
| 234 | + fields: Dict[str, str], key: Optional[ExecutionKey] | ||
| 235 | +) -> Tuple[Optional[StepRecord], Optional[str]]: | ||
| 236 | + numbers: Dict[str, int] = {} | ||
| 237 | + for name in NUMERIC_FIELDS: | ||
| 238 | + value = fields[name] | ||
| 239 | + if re.fullmatch(r"-?[0-9]+", value) is None: | ||
| 240 | + return (None, "字段 {} 不是整数".format(name)) | ||
| 241 | + numbers[name] = int(value) | ||
| 242 | + if numbers["end_us"] < numbers["start_us"]: | ||
| 243 | + return (None, "end_us 小于 start_us") | ||
| 244 | + if numbers["cost_us"] != numbers["end_us"] - numbers["start_us"]: | ||
| 245 | + return (None, "cost_us 与时间区间不一致") | ||
| 246 | + record = StepRecord(api=fields["api"], mode=fields["mode"], key=key, **numbers) | ||
| 247 | + return (record, None) | ||
| 248 | + | ||
| 249 | + | ||
| 250 | +def collect_records(paths: Sequence[Path]) -> Tuple[List[StepRecord], List[str]]: | ||
| 251 | + """合并多个日志文件中的 STEP 记录,按 (执行对象, 步骤, 接口) 去重。""" | ||
| 252 | + unique: Dict[Tuple[ExecutionKey, int, str], StepRecord] = {} | ||
| 253 | + errors: List[str] = [] | ||
| 254 | + for path in paths: | ||
| 255 | + try: | ||
| 256 | + with path.open("r", encoding="utf-8", errors="replace") as source: | ||
| 257 | + for line_no, line in enumerate(source, 1): | ||
| 258 | + record, error = parse_step_line(line) | ||
| 259 | + if record is not None: | ||
| 260 | + unique.setdefault((record.key, record.step, record.api), record) | ||
| 261 | + elif error is not None: | ||
| 262 | + errors.append("{}:{} {}".format(path.name, line_no, error)) | ||
| 263 | + except OSError as error: | ||
| 264 | + errors.append("无法读取 {}:{}".format(path, error)) | ||
| 265 | + records = sorted(unique.values(), key=lambda item: (item.key, item.step)) | ||
| 266 | + return (records, errors) | ||
| 267 | + | ||
| 268 | + | ||
| 269 | +def log_files(stdout_path: Path, plog_dir: Path) -> List[Path]: | ||
| 270 | + paths = [stdout_path] if stdout_path.is_file() else [] | ||
| 271 | + if plog_dir.is_dir(): | ||
| 272 | + paths.extend(sorted(path for path in plog_dir.rglob("*") if path.is_file())) | ||
| 273 | + return paths | ||
| 274 | + | ||
| 275 | + | ||
| 276 | +# ---------------------------------------------------------------- 单次执行 | ||
| 277 | + | ||
| 278 | + | ||
| 279 | +def trial_environment(config: str, plog_dir: Path) -> Dict[str, str]: | ||
| 280 | + """覆盖父进程可能残留的旧配置,并把 plog 收敛到本次执行目录。""" | ||
| 281 | + environment = os.environ.copy() | ||
| 282 | + environment[MODE_ENV] = config | ||
| 283 | + environment["ASCEND_PROCESS_LOG_PATH"] = str(plog_dir) | ||
| 284 | + environment.setdefault("ASCEND_SLOG_PRINT_TO_STDOUT", "0") | ||
| 285 | + return environment | ||
| 286 | + | ||
| 287 | + | ||
| 288 | +def run_command( | ||
| 289 | + argv: Sequence[str], stdout_path: Path, env: Dict[str, str], timeout: int | ||
| 290 | +): | ||
| 291 | + """执行被测命令,stdout/stderr 合并落盘,返回 (退出码, 启动错误, 墙上耗时)。""" | ||
| 292 | + start = time.monotonic() | ||
| 293 | + process = None | ||
| 294 | + exit_code: Optional[int] = None | ||
| 295 | + launch_error: Optional[str] = None | ||
| 296 | + with stdout_path.open("w", encoding="utf-8") as output: | ||
| 297 | + try: | ||
| 298 | + process = subprocess.Popen( | ||
| 299 | + list(argv), | ||
| 300 | + stdout=output, | ||
| 301 | + stderr=subprocess.STDOUT, | ||
| 302 | + env=env, | ||
| 303 | + start_new_session=True, | ||
| 304 | + ) | ||
| 305 | + exit_code = process.wait(timeout=timeout if timeout > 0 else None) | ||
| 306 | + except subprocess.TimeoutExpired: | ||
| 307 | + launch_error = "执行超时({} 秒)".format(timeout) | ||
| 308 | + except OSError as error: | ||
| 309 | + launch_error = "子进程启动失败:{}".format(error) | ||
| 310 | + finally: | ||
| 311 | + if process is not None and process.poll() is None: | ||
| 312 | + terminate(process) | ||
| 313 | + exit_code = process.returncode | ||
| 314 | + return (exit_code, launch_error, time.monotonic() - start) | ||
| 315 | + | ||
| 316 | + | ||
| 317 | +def terminate(process: subprocess.Popen) -> None: | ||
| 318 | + for send_signal in (process.terminate, process.kill): | ||
| 319 | + try: | ||
| 320 | + send_signal() | ||
| 321 | + process.wait(timeout=5.0) | ||
| 322 | + return | ||
| 323 | + except (OSError, subprocess.TimeoutExpired): | ||
| 324 | + continue | ||
| 325 | + | ||
| 326 | + | ||
| 327 | +def prepare_trial( | ||
| 328 | + config: str, repeat: int, index: int, args: argparse.Namespace, detail: str | ||
| 329 | +) -> Tuple[Path, Path, Path]: | ||
| 330 | + directory = args.output_dir / "trial_{:03d}_{}_r{}".format( | ||
| 331 | + index, config.replace(":", ""), repeat | ||
| 332 | + ) | ||
| 333 | + plog_dir = directory / "plog" | ||
| 334 | + plog_dir.mkdir(parents=True) | ||
| 335 | + print("[{:03d}] 配置={} 第 {} 轮:{}".format(index, config, repeat, detail)) | ||
| 336 | + return (directory, plog_dir, directory / "stdout.log") | ||
| 337 | + | ||
| 338 | + | ||
| 339 | +def finish_trial( | ||
| 340 | + config: str, | ||
| 341 | + repeat: int, | ||
| 342 | + exit_code: Optional[int], | ||
| 343 | + wall_seconds: float, | ||
| 344 | + paths: Tuple[Path, Path, Path], | ||
| 345 | + errors: List[str], | ||
| 346 | + args: argparse.Namespace, | ||
| 347 | +) -> TrialResult: | ||
| 348 | + """解析日志、判定有效性并落盘,在线与离线共用。""" | ||
| 349 | + directory, plog_dir, stdout_path = paths | ||
| 350 | + records, parse_errors = collect_records(log_files(stdout_path, plog_dir)) | ||
| 351 | + result = evaluate_trial( | ||
| 352 | + config, repeat, exit_code, wall_seconds, records, errors + parse_errors, args | ||
| 353 | + ) | ||
| 354 | + write_steps_csv(directory / "steps.csv", records) | ||
| 355 | + print( | ||
| 356 | + " 退出码={} STEP={} 有效={} 耗时={:.1f}s{}".format( | ||
| 357 | + exit_code, | ||
| 358 | + len(records), | ||
| 359 | + "是" if result.valid else "否", | ||
| 360 | + wall_seconds, | ||
| 361 | + "" if result.valid else ",原因:" + ";".join(result.reasons), | ||
| 362 | + ) | ||
| 363 | + ) | ||
| 364 | + return result | ||
| 365 | + | ||
| 366 | + | ||
| 367 | +def run_trial( | ||
| 368 | + config: str, repeat: int, index: int, args: argparse.Namespace | ||
| 369 | +) -> TrialResult: | ||
| 370 | + """在线执行:直接在本机运行被测命令。""" | ||
| 371 | + paths = prepare_trial(config, repeat, index, args, args.command_text) | ||
| 372 | + exit_code, launch_error, wall_seconds = run_command( | ||
| 373 | + args.argv, paths[2], trial_environment(config, paths[1]), args.timeout | ||
| 374 | + ) | ||
| 375 | + errors = [launch_error] if launch_error is not None else [] | ||
| 376 | + return finish_trial(config, repeat, exit_code, wall_seconds, paths, errors, args) | ||
| 377 | + | ||
| 378 | + | ||
| 379 | +def evaluate_trial( | ||
| 380 | + config: str, | ||
| 381 | + repeat: int, | ||
| 382 | + exit_code: Optional[int], | ||
| 383 | + wall_seconds: float, | ||
| 384 | + records: Sequence[StepRecord], | ||
| 385 | + errors: Sequence[str], | ||
| 386 | + args: argparse.Namespace, | ||
| 387 | +) -> TrialResult: | ||
| 388 | + """对一次执行做数据有效性检查,只有全部通过的数据才参与排名。""" | ||
| 389 | + reasons: List[str] = [] | ||
| 390 | + if exit_code != 0: | ||
| 391 | + reasons.append("退出码非 0({})".format(exit_code)) | ||
| 392 | + if errors: | ||
| 393 | + reasons.append("{} 条日志异常,首条:{}".format(len(errors), errors[0])) | ||
| 394 | + retained = [record for record in records if record.step >= args.drop_first] | ||
| 395 | + mismatched = sorted({record.mode for record in retained if record.mode != config}) | ||
| 396 | + if mismatched: | ||
| 397 | + reasons.append( | ||
| 398 | + "STEP mode 与候选 {} 不一致:{}(确认寻优 Pass 已安装)".format( | ||
| 399 | + config, ",".join(mismatched) | ||
| 400 | + ) | ||
| 401 | + ) | ||
| 402 | + if any(record.ret != 0 or record.sync_ret != 0 for record in retained): | ||
| 403 | + reasons.append("存在 ret/sync_ret 非 0 的 STEP") | ||
| 404 | + main_key = choose_main_key(retained, args.main_graph) | ||
| 405 | + costs = [record.cost_us for record in retained if record.key == main_key] | ||
| 406 | + if main_key is None and args.main_graph is not None and retained: | ||
| 407 | + reasons.append("指定的主执行对象 {} 不存在".format(key_text(args.main_graph))) | ||
| 408 | + elif main_key is None: | ||
| 409 | + reasons.append("没有可统计的 STEP 记录") | ||
| 410 | + elif len(costs) < args.min_steps: | ||
| 411 | + reasons.append( | ||
| 412 | + "主执行对象仅 {} 个有效步骤,少于 {}".format(len(costs), args.min_steps) | ||
| 413 | + ) | ||
| 414 | + elif overlapped(retained, main_key): | ||
| 415 | + reasons.append("主执行对象的 STEP 时间区间重叠,无法作为串行耗时统计") | ||
| 416 | + return TrialResult( | ||
| 417 | + config, repeat, exit_code, wall_seconds, len(records), main_key, costs, reasons | ||
| 418 | + ) | ||
| 419 | + | ||
| 420 | + | ||
| 421 | +def choose_main_key( | ||
| 422 | + records: Sequence[StepRecord], explicit: Optional[ExecutionKey] | ||
| 423 | +) -> Optional[ExecutionKey]: | ||
| 424 | + """默认取 STEP 数最多的执行对象,多个执行对象时可用 --main-graph 指定。""" | ||
| 425 | + counts: Dict[ExecutionKey, int] = {} | ||
| 426 | + for record in records: | ||
| 427 | + counts[record.key] = counts.get(record.key, 0) + 1 | ||
| 428 | + if explicit is not None: | ||
| 429 | + return explicit if explicit in counts else None | ||
| 430 | + if not counts: | ||
| 431 | + return None | ||
| 432 | + return min(counts, key=lambda key: (-counts[key], key)) | ||
| 433 | + | ||
| 434 | + | ||
| 435 | +def overlapped(records: Sequence[StepRecord], key: Optional[ExecutionKey]) -> bool: | ||
| 436 | + ordered = sorted( | ||
| 437 | + (record for record in records if record.key == key), | ||
| 438 | + key=lambda item: (item.start_us, item.end_us), | ||
| 439 | + ) | ||
| 440 | + latest_end = 0 | ||
| 441 | + for record in ordered: | ||
| 442 | + if record.start_us < latest_end: | ||
| 443 | + return True | ||
| 444 | + latest_end = max(latest_end, record.end_us) | ||
| 445 | + return False | ||
| 446 | + | ||
| 447 | + | ||
| 448 | +def parse_main_graph(value: Optional[str]) -> Optional[ExecutionKey]: | ||
| 449 | + if not value: | ||
| 450 | + return None | ||
| 451 | + model = re.fullmatch(r"model:([0-9]+)", value.strip()) | ||
| 452 | + if model is not None: | ||
| 453 | + return ("model", int(model.group(1)), 0) | ||
| 454 | + graph = re.fullmatch(r"([0-9]+):([0-9]+)", value.strip()) | ||
| 455 | + if graph is not None: | ||
| 456 | + return ("graph", int(graph.group(1)), int(graph.group(2))) | ||
| 457 | + raise AutotuneError("--main-graph 需形如 session_id:graph_id 或 model:model_id。") | ||
| 458 | + | ||
| 459 | + | ||
| 460 | +# ---------------------------------------------------------------- 离线目标机 | ||
| 461 | + | ||
| 462 | + | ||
| 463 | +def load_target(path_value: str) -> Target: | ||
| 464 | + """读取目标机配置;密钥优先,密码只从环境变量取,不落配置文件。""" | ||
| 465 | + path = Path(path_value).expanduser() | ||
| 466 | + try: | ||
| 467 | + raw = json.loads(path.read_text(encoding="utf-8")) | ||
| 468 | + except OSError as error: | ||
| 469 | + raise AutotuneError("无法读取目标机配置 {}:{}".format(path, error)) from error | ||
| 470 | + except ValueError as error: | ||
| 471 | + raise AutotuneError("目标机配置不是合法 JSON:{}".format(error)) from error | ||
| 472 | + if not isinstance(raw, dict): | ||
| 473 | + raise AutotuneError("目标机配置需为 JSON 对象。") | ||
| 474 | + target = Target( | ||
| 475 | + host=target_text(raw, "host"), | ||
| 476 | + user=target_text(raw, "user"), | ||
| 477 | + remote_workdir=target_text(raw, "remote_workdir"), | ||
| 478 | + run_command=target_text(raw, "run_command"), | ||
| 479 | + port=int(raw.get("port", 22)), | ||
| 480 | + identity_file=target_identity(raw), | ||
| 481 | + cann_env=str(raw["cann_env"]) if raw.get("cann_env") else None, | ||
| 482 | + password=os.environ.get(PASSWORD_ENV) or None, | ||
| 483 | + ) | ||
| 484 | + validate_target(target) | ||
| 485 | + return target | ||
| 486 | + | ||
| 487 | + | ||
| 488 | +def target_text(raw: Dict[str, object], name: str) -> str: | ||
| 489 | + value = raw.get(name) | ||
| 490 | + if not isinstance(value, str) or not value.strip(): | ||
| 491 | + raise AutotuneError("目标机配置缺少字符串字段 {}。".format(name)) | ||
| 492 | + return value.strip() | ||
| 493 | + | ||
| 494 | + | ||
| 495 | +def target_identity(raw: Dict[str, object]) -> Optional[Path]: | ||
| 496 | + value = raw.get("identity_file") | ||
| 497 | + if not value: | ||
| 498 | + return None | ||
| 499 | + identity = Path(str(value)).expanduser() | ||
| 500 | + if not identity.is_file(): | ||
| 501 | + raise AutotuneError("配置的私钥不存在:{}。".format(identity)) | ||
| 502 | + return identity | ||
| 503 | + | ||
| 504 | + | ||
| 505 | +def validate_target(target: Target) -> None: | ||
| 506 | + if not 1 <= target.port <= 65535: | ||
| 507 | + raise AutotuneError("目标机端口非法:{}。".format(target.port)) | ||
| 508 | + parts = [item for item in target.remote_workdir.split("/") if item] | ||
| 509 | + if not target.remote_workdir.startswith("/") or len(parts) < 2: | ||
| 510 | + raise AutotuneError( | ||
| 511 | + "remote_workdir 需为层级不少于两级的绝对路径(寻优结束会整目录删除)。" | ||
| 512 | + ) | ||
| 513 | + if "{om}" not in target.run_command: | ||
| 514 | + raise AutotuneError("目标机 run_command 必须包含 {om} 占位符。") | ||
| 515 | + if target.identity_file is None and target.password is None: | ||
| 516 | + print( | ||
| 517 | + "[提示] 未配置 identity_file,也未设置 {},将使用 ssh 默认密钥。".format( | ||
| 518 | + PASSWORD_ENV | ||
| 519 | + ) | ||
| 520 | + ) | ||
| 521 | + if target.identity_file is None and target.password is not None: | ||
| 522 | + if shutil.which("sshpass") is None: | ||
| 523 | + raise AutotuneError("密码认证需要 sshpass,请安装后重试,或改用密钥认证。") | ||
| 524 | + | ||
| 525 | + | ||
| 526 | +def ssh_options(target: Target) -> Tuple[List[str], List[str]]: | ||
| 527 | + """返回 (sshpass 前缀, 公共选项);配置了私钥时不走密码。""" | ||
| 528 | + use_password = target.identity_file is None and target.password is not None | ||
| 529 | + options = ["-o", "StrictHostKeyChecking=accept-new", "-o", "ConnectTimeout=15"] | ||
| 530 | + if not use_password: | ||
| 531 | + options.extend(["-o", "BatchMode=yes"]) | ||
| 532 | + if target.identity_file is not None: | ||
| 533 | + options.extend(["-i", str(target.identity_file)]) | ||
| 534 | + return (["sshpass", "-e"] if use_password else [], options) | ||
| 535 | + | ||
| 536 | + | ||
| 537 | +def ssh_argv(target: Target, script: str) -> List[str]: | ||
| 538 | + prefix, options = ssh_options(target) | ||
| 539 | + return ( | ||
| 540 | + prefix | ||
| 541 | + + ["ssh"] | ||
| 542 | + + options | ||
| 543 | + + ["-p", str(target.port), target.destination, script] | ||
| 544 | + ) | ||
| 545 | + | ||
| 546 | + | ||
| 547 | +def scp_argv(target: Target, sources: Sequence[str], destination: str) -> List[str]: | ||
| 548 | + prefix, options = ssh_options(target) | ||
| 549 | + argv = prefix + ["scp", "-r"] + options + ["-P", str(target.port)] | ||
| 550 | + return argv + list(sources) + [destination] | ||
| 551 | + | ||
| 552 | + | ||
| 553 | +def target_environment(target: Target) -> Dict[str, str]: | ||
| 554 | + """密码经 SSHPASS 传给 sshpass,不出现在命令行里。""" | ||
| 555 | + environment = os.environ.copy() | ||
| 556 | + environment.pop(PASSWORD_ENV, None) | ||
| 557 | + if target.identity_file is None and target.password is not None: | ||
| 558 | + environment["SSHPASS"] = target.password | ||
| 559 | + return environment | ||
| 560 | + | ||
| 561 | + | ||
| 562 | +def run_stage( | ||
| 563 | + argv: Sequence[str], log_path: Path, env: Dict[str, str], timeout: int, action: str | ||
| 564 | +) -> None: | ||
| 565 | + exit_code, launch_error, _ = run_command(argv, log_path, env, timeout) | ||
| 566 | + if launch_error is not None or exit_code != 0: | ||
| 567 | + raise AutotuneError( | ||
| 568 | + "{}失败(退出码={}):{}".format( | ||
| 569 | + action, | ||
| 570 | + "未启动" if exit_code is None else exit_code, | ||
| 571 | + launch_error or "详见 {}".format(log_path), | ||
| 572 | + ) | ||
| 573 | + ) | ||
| 574 | + | ||
| 575 | + | ||
| 576 | +def compile_candidates( | ||
| 577 | + configs: Sequence[str], args: argparse.Namespace | ||
| 578 | +) -> Dict[str, Path]: | ||
| 579 | + """在编译机逐候选编译 OM,候选经环境变量注入,属性由自定义 Pass 写进模型。""" | ||
| 580 | + args.om_dir.mkdir(parents=True, exist_ok=True) | ||
| 581 | + oms: Dict[str, Path] = {} | ||
| 582 | + for config in configs: | ||
| 583 | + slug = config.replace(":", "") | ||
| 584 | + prefix = args.om_dir / "model_{}".format(slug) | ||
| 585 | + om_path = prefix.with_suffix(".om") | ||
| 586 | + argv = [ | ||
| 587 | + item.format(om=str(om_path), om_prefix=str(prefix)) | ||
| 588 | + for item in args.compile_argv | ||
| 589 | + ] | ||
| 590 | + environment = os.environ.copy() | ||
| 591 | + environment[MODE_ENV] = config | ||
| 592 | + print("[编译] 候选={} → {}".format(config, om_path.name)) | ||
| 593 | + run_stage( | ||
| 594 | + argv, | ||
| 595 | + args.om_dir / "compile_{}.log".format(slug), | ||
| 596 | + environment, | ||
| 597 | + args.timeout, | ||
| 598 | + "候选 {} 编译".format(config), | ||
| 599 | + ) | ||
| 600 | + if not om_path.is_file(): | ||
| 601 | + raise AutotuneError( | ||
| 602 | + "候选 {} 未产出 {},检查 --compile-command 的输出路径。".format( | ||
| 603 | + config, om_path | ||
| 604 | + ) | ||
| 605 | + ) | ||
| 606 | + oms[config] = om_path | ||
| 607 | + return oms | ||
| 608 | + | ||
| 609 | + | ||
| 610 | +def upload_candidates(oms: Dict[str, Path], args: argparse.Namespace) -> Dict[str, str]: | ||
| 611 | + """一次性把全部候选 OM 传到目标机,返回候选到远端路径的映射。""" | ||
| 612 | + target = args.target | ||
| 613 | + remote_om_dir = "{}/om".format(target.remote_workdir) | ||
| 614 | + environment = target_environment(target) | ||
| 615 | + print("[部署] 上传 {} 个 OM 到 {}:{}".format(len(oms), target.host, remote_om_dir)) | ||
| 616 | + run_stage( | ||
| 617 | + ssh_argv(target, "mkdir -p {}".format(remote_om_dir)), | ||
| 618 | + args.output_dir / "target_prepare.log", | ||
| 619 | + environment, | ||
| 620 | + args.timeout, | ||
| 621 | + "创建远端目录", | ||
| 622 | + ) | ||
| 623 | + run_stage( | ||
| 624 | + scp_argv( | ||
| 625 | + target, | ||
| 626 | + [str(path) for path in oms.values()], | ||
| 627 | + "{}:{}/".format(target.destination, remote_om_dir), | ||
| 628 | + ), | ||
| 629 | + args.output_dir / "target_upload.log", | ||
| 630 | + environment, | ||
| 631 | + args.timeout, | ||
| 632 | + "上传 OM", | ||
| 633 | + ) | ||
| 634 | + return { | ||
| 635 | + config: "{}/{}".format(remote_om_dir, path.name) for config, path in oms.items() | ||
| 636 | + } | ||
| 637 | + | ||
| 638 | + | ||
| 639 | +def remote_script(target: Target, remote_om: str, remote_plog: str) -> str: | ||
| 640 | + """远端执行脚本:每轮先清空 plog,避免上一轮记录与本轮撞键被静默丢弃。""" | ||
| 641 | + lines = [ | ||
| 642 | + "set -e", | ||
| 643 | + "rm -rf {0}".format(remote_plog), | ||
| 644 | + "mkdir -p {0}".format(remote_plog), | ||
| 645 | + ] | ||
| 646 | + if target.cann_env: | ||
| 647 | + lines.append(". {}".format(target.cann_env)) | ||
| 648 | + lines.append("export ASCEND_PROCESS_LOG_PATH={}".format(remote_plog)) | ||
| 649 | + lines.append("export ASCEND_SLOG_PRINT_TO_STDOUT=0") | ||
| 650 | + lines.append(target.run_command.format(om=remote_om)) | ||
| 651 | + return "\n".join(lines) | ||
| 652 | + | ||
| 653 | + | ||
| 654 | +def run_offline_trial( | ||
| 655 | + config: str, repeat: int, index: int, args: argparse.Namespace, remote_om: str | ||
| 656 | +) -> TrialResult: | ||
| 657 | + """离线执行:在目标机跑候选 OM,再把 plog 回传到本轮目录解析。""" | ||
| 658 | + target = args.target | ||
| 659 | + remote_plog = "{}/plog".format(target.remote_workdir) | ||
| 660 | + paths = prepare_trial( | ||
| 661 | + config, repeat, index, args, "{} @ {}".format(remote_om, target.host) | ||
| 662 | + ) | ||
| 663 | + environment = target_environment(target) | ||
| 664 | + exit_code, launch_error, wall_seconds = run_command( | ||
| 665 | + ssh_argv(target, remote_script(target, remote_om, remote_plog)), | ||
| 666 | + paths[2], | ||
| 667 | + environment, | ||
| 668 | + args.timeout, | ||
| 669 | + ) | ||
| 670 | + errors = [launch_error] if launch_error is not None else [] | ||
| 671 | + fetch_error = fetch_remote_plog(target, remote_plog, paths, environment, args) | ||
| 672 | + if fetch_error is not None: | ||
| 673 | + errors.append(fetch_error) | ||
| 674 | + return finish_trial(config, repeat, exit_code, wall_seconds, paths, errors, args) | ||
| 675 | + | ||
| 676 | + | ||
| 677 | +def fetch_remote_plog( | ||
| 678 | + target: Target, | ||
| 679 | + remote_plog: str, | ||
| 680 | + paths: Tuple[Path, Path, Path], | ||
| 681 | + env: Dict[str, str], | ||
| 682 | + args: argparse.Namespace, | ||
| 683 | +) -> Optional[str]: | ||
| 684 | + """回传失败只记为本轮日志异常,不中断整体寻优。""" | ||
| 685 | + directory, plog_dir, _ = paths | ||
| 686 | + argv = scp_argv( | ||
| 687 | + target, | ||
| 688 | + ["{}:{}/.".format(target.destination, remote_plog)], | ||
| 689 | + str(plog_dir), | ||
| 690 | + ) | ||
| 691 | + log_path = directory / "fetch_plog.log" | ||
| 692 | + exit_code, launch_error, _ = run_command(argv, log_path, env, args.timeout) | ||
| 693 | + if launch_error is not None or exit_code != 0: | ||
| 694 | + return "回传远端 plog 失败(退出码={}):{}".format( | ||
| 695 | + exit_code, launch_error or "详见 {}".format(log_path.name) | ||
| 696 | + ) | ||
| 697 | + return None | ||
| 698 | + | ||
| 699 | + | ||
| 700 | +def cleanup_remote(args: argparse.Namespace) -> None: | ||
| 701 | + target = args.target | ||
| 702 | + try: | ||
| 703 | + run_stage( | ||
| 704 | + ssh_argv(target, "rm -rf {}".format(target.remote_workdir)), | ||
| 705 | + args.output_dir / "target_cleanup.log", | ||
| 706 | + target_environment(target), | ||
| 707 | + args.timeout, | ||
| 708 | + "清理远端目录", | ||
| 709 | + ) | ||
| 710 | + print("[部署] 已清理远端目录 {}:{}".format(target.host, target.remote_workdir)) | ||
| 711 | + except AutotuneError as error: | ||
| 712 | + print("[警告] {}".format(error)) | ||
| 713 | + | ||
| 714 | + | ||
| 715 | +# ---------------------------------------------------------------- 汇总与推荐 | ||
| 716 | + | ||
| 717 | + | ||
| 718 | +def summarize(config: str, results: Sequence[TrialResult]) -> ConfigSummary: | ||
| 719 | + """合并同一候选各轮的有效步骤耗时。""" | ||
| 720 | + valid = [result for result in results if result.valid] | ||
| 721 | + costs = [cost for result in valid for cost in result.costs] | ||
| 722 | + summary = ConfigSummary(config, len(results), len(valid), len(costs)) | ||
| 723 | + if not costs: | ||
| 724 | + summary.reasons = sorted( | ||
| 725 | + {reason for item in results for reason in item.reasons} | ||
| 726 | + ) | ||
| 727 | + return summary | ||
| 728 | + ordered = sorted(costs) | ||
| 729 | + summary.mean_us = statistics.mean(ordered) | ||
| 730 | + summary.median_us = statistics.median(ordered) | ||
| 731 | + summary.p90_us = float(ordered[max(0, math.ceil(0.9 * len(ordered)) - 1)]) | ||
| 732 | + stddev = statistics.pstdev(ordered) | ||
| 733 | + summary.cv = stddev / summary.mean_us if summary.mean_us > 0 else None | ||
| 734 | + return summary | ||
| 735 | + | ||
| 736 | + | ||
| 737 | +def apply_speedup(summaries: Sequence[ConfigSummary]) -> Optional[ConfigSummary]: | ||
| 738 | + baseline = next( | ||
| 739 | + ( | ||
| 740 | + item | ||
| 741 | + for item in summaries | ||
| 742 | + if item.config == BASELINE_CONFIG and item.median_us | ||
| 743 | + ), | ||
| 744 | + None, | ||
| 745 | + ) | ||
| 746 | + if baseline is None: | ||
| 747 | + return None | ||
| 748 | + for summary in summaries: | ||
| 749 | + if summary.median_us: | ||
| 750 | + summary.speedup = baseline.median_us / summary.median_us | ||
| 751 | + return baseline | ||
| 752 | + | ||
| 753 | + | ||
| 754 | +def verdict(summary: ConfigSummary) -> str: | ||
| 755 | + if summary.median_us is None: | ||
| 756 | + return "数据无效" | ||
| 757 | + if summary.speedup is None: | ||
| 758 | + return "无基准" | ||
| 759 | + if summary.speedup >= POSITIVE_SPEEDUP: | ||
| 760 | + return "提升" | ||
| 761 | + if summary.speedup >= NEUTRAL_SPEEDUP: | ||
| 762 | + return "持平" | ||
| 763 | + return "劣化" | ||
| 764 | + | ||
| 765 | + | ||
| 766 | +def rank(summaries: List[ConfigSummary]) -> List[ConfigSummary]: | ||
| 767 | + return sorted( | ||
| 768 | + summaries, | ||
| 769 | + key=lambda item: (item.median_us is None, item.median_us or 0.0, item.config), | ||
| 770 | + ) | ||
| 771 | + | ||
| 772 | + | ||
| 773 | +def recommend(summaries: Sequence[ConfigSummary]) -> Optional[ConfigSummary]: | ||
| 774 | + candidates = [ | ||
| 775 | + item | ||
| 776 | + for item in summaries | ||
| 777 | + if item.config != BASELINE_CONFIG | ||
| 778 | + and item.median_us is not None | ||
| 779 | + and (item.speedup or 0.0) >= POSITIVE_SPEEDUP | ||
| 780 | + ] | ||
| 781 | + if not candidates: | ||
| 782 | + return None | ||
| 783 | + return min(candidates, key=lambda item: item.median_us) | ||
| 784 | + | ||
| 785 | + | ||
| 786 | +# ---------------------------------------------------------------- 输出 | ||
| 787 | + | ||
| 788 | + | ||
| 789 | +def write_steps_csv(path: Path, records: Sequence[StepRecord]) -> None: | ||
| 790 | + with path.open("w", encoding="utf-8", newline="") as output: | ||
| 791 | + writer = csv.writer(output) | ||
| 792 | + writer.writerow(("execution",) + REQUIRED_FIELDS) | ||
| 793 | + for item in records: | ||
| 794 | + values = [getattr(item, name) for name in REQUIRED_FIELDS] | ||
| 795 | + writer.writerow([key_text(item.key)] + values) | ||
| 796 | + | ||
| 797 | + | ||
| 798 | +def key_text(key: Optional[ExecutionKey]) -> str: | ||
| 799 | + if key is None: | ||
| 800 | + return "-" | ||
| 801 | + if key[0] == "model": | ||
| 802 | + return "model:{}".format(key[1]) | ||
| 803 | + return "{}:{}".format(key[1], key[2]) | ||
| 804 | + | ||
| 805 | + | ||
| 806 | +def summary_row(summary: ConfigSummary) -> Dict[str, object]: | ||
| 807 | + return { | ||
| 808 | + "config": summary.config, | ||
| 809 | + "trials": summary.trials, | ||
| 810 | + "valid_trials": summary.valid_trials, | ||
| 811 | + "steps": summary.steps, | ||
| 812 | + "mean_us": round(summary.mean_us, 2) if summary.mean_us else None, | ||
| 813 | + "median_us": round(summary.median_us, 2) if summary.median_us else None, | ||
| 814 | + "p90_us": summary.p90_us, | ||
| 815 | + "cv": round(summary.cv, 4) if summary.cv else None, | ||
| 816 | + "speedup": round(summary.speedup, 4) if summary.speedup else None, | ||
| 817 | + "verdict": verdict(summary), | ||
| 818 | + "reasons": ";".join(summary.reasons), | ||
| 819 | + } | ||
| 820 | + | ||
| 821 | + | ||
| 822 | +def write_summaries( | ||
| 823 | + output_dir: Path, | ||
| 824 | + summaries: Sequence[ConfigSummary], | ||
| 825 | + results: Sequence[TrialResult], | ||
| 826 | + args: argparse.Namespace, | ||
| 827 | +) -> None: | ||
| 828 | + rows = [summary_row(summary) for summary in summaries] | ||
| 829 | + with (output_dir / "summary.csv").open("w", encoding="utf-8", newline="") as output: | ||
| 830 | + writer = csv.DictWriter(output, fieldnames=list(rows[0].keys())) | ||
| 831 | + writer.writeheader() | ||
| 832 | + writer.writerows(rows) | ||
| 833 | + best = recommend(summaries) | ||
| 834 | + document = { | ||
| 835 | + "mode": args.mode, | ||
| 836 | + "command": args.command_text, | ||
| 837 | + "compile_command": args.compile_command, | ||
| 838 | + "target_host": args.target.host if args.mode == "offline" else None, | ||
| 839 | + "repeat": args.repeat, | ||
| 840 | + "drop_first": args.drop_first, | ||
| 841 | + "min_steps": args.min_steps, | ||
| 842 | + "configs": [summary.config for summary in summaries], | ||
| 843 | + "summaries": rows, | ||
| 844 | + "recommended": best.config if best is not None else BASELINE_CONFIG, | ||
| 845 | + "trials": [ | ||
| 846 | + { | ||
| 847 | + "config": result.config, | ||
| 848 | + "repeat": result.repeat, | ||
| 849 | + "exit_code": result.exit_code, | ||
| 850 | + "wall_seconds": round(result.wall_seconds, 3), | ||
| 851 | + "step_count": result.step_count, | ||
| 852 | + "main_graph": key_text(result.main_key), | ||
| 853 | + "valid": result.valid, | ||
| 854 | + "reasons": result.reasons, | ||
| 855 | + } | ||
| 856 | + for result in results | ||
| 857 | + ], | ||
| 858 | + } | ||
| 859 | + (output_dir / "summary.json").write_text( | ||
| 860 | + json.dumps(document, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" | ||
| 861 | + ) | ||
| 862 | + | ||
| 863 | + | ||
| 864 | +def print_report(summaries: Sequence[ConfigSummary], args: argparse.Namespace) -> None: | ||
| 865 | + header = ( | ||
| 866 | + "配置", | ||
| 867 | + "有效轮次", | ||
| 868 | + "步数", | ||
| 869 | + "平均(ms)", | ||
| 870 | + "中位(ms)", | ||
| 871 | + "P90(ms)", | ||
| 872 | + "CV", | ||
| 873 | + "加速比", | ||
| 874 | + "结论", | ||
| 875 | + ) | ||
| 876 | + widths = (24, 10, 6, 10, 10, 10, 8, 8, 8) | ||
| 877 | + print("\n寻优结果(按中位耗时升序):") | ||
| 878 | + print(" ".join(pad(name, width) for name, width in zip(header, widths))) | ||
| 879 | + for summary in summaries: | ||
| 880 | + cells = ( | ||
| 881 | + summary.config, | ||
| 882 | + "{}/{}".format(summary.valid_trials, summary.trials), | ||
| 883 | + str(summary.steps), | ||
| 884 | + millis(summary.mean_us), | ||
| 885 | + millis(summary.median_us), | ||
| 886 | + millis(summary.p90_us), | ||
| 887 | + "{:.3f}".format(summary.cv) if summary.cv is not None else "-", | ||
| 888 | + "{:.3f}".format(summary.speedup) if summary.speedup is not None else "-", | ||
| 889 | + verdict(summary), | ||
| 890 | + ) | ||
| 891 | + print(" ".join(pad(cell, width) for cell, width in zip(cells, widths))) | ||
| 892 | + print_recommendation(summaries, args) | ||
| 893 | + | ||
| 894 | + | ||
| 895 | +def pad(text: str, width: int) -> str: | ||
| 896 | + """按终端显示宽度补齐,中文按两列计算。""" | ||
| 897 | + shown = sum(2 if unicodedata.east_asian_width(char) in "WF" else 1 for char in text) | ||
| 898 | + return text + " " * max(0, width - shown) | ||
| 899 | + | ||
| 900 | + | ||
| 901 | +def millis(value: Optional[float]) -> str: | ||
| 902 | + return "-" if value is None else "{:.3f}".format(value / 1000.0) | ||
| 903 | + | ||
| 904 | + | ||
| 905 | +def print_recommendation( | ||
| 906 | + summaries: Sequence[ConfigSummary], args: argparse.Namespace | ||
| 907 | +) -> None: | ||
| 908 | + invalid = [item for item in summaries if item.median_us is None] | ||
| 909 | + for item in invalid: | ||
| 910 | + print( | ||
| 911 | + "[警告] 候选 {} 无有效数据:{}".format(item.config, ";".join(item.reasons)) | ||
| 912 | + ) | ||
| 913 | + best = recommend(summaries) | ||
| 914 | + if best is None: | ||
| 915 | + print( | ||
| 916 | + "\n[结论] 没有候选相对 default 取得 {:.0%} 以上收益,建议保持默认配置。".format( | ||
| 917 | + POSITIVE_SPEEDUP - 1 | ||
| 918 | + ) | ||
| 919 | + ) | ||
| 920 | + return | ||
| 921 | + print( | ||
| 922 | + "\n[结论] 推荐配置:{},相对 default 加速比 {:.3f},中位耗时 {} ms。".format( | ||
| 923 | + best.config, best.speedup, millis(best.median_us) | ||
| 924 | + ) | ||
| 925 | + ) | ||
| 926 | + if best.cv is not None and best.cv > 0.05: | ||
| 927 | + print( | ||
| 928 | + "[提醒] 该候选耗时波动较大(CV={:.3f}),建议增大 --repeat 复测。".format( | ||
| 929 | + best.cv | ||
| 930 | + ) | ||
| 931 | + ) | ||
| 932 | + if args.mode == "offline": | ||
| 933 | + print( | ||
| 934 | + "[复现] 编译端 {}={} {}".format(MODE_ENV, best.config, args.compile_command) | ||
| 935 | + ) | ||
| 936 | + print(" 目标机执行 {}".format(args.command_text)) | ||
| 937 | + else: | ||
| 938 | + print("[复现] {}={} {}".format(MODE_ENV, best.config, args.command_text)) | ||
| 939 | + print( | ||
| 940 | + "[落地] 生产态请直接把根图属性 ge.autoMultistreamParallelMode 置为 {},".format( | ||
| 941 | + best.config | ||
| 942 | + ) | ||
| 943 | + ) | ||
| 944 | + print(" 不要保留寻优 Pass 与调测打点(打点含同步等待,会影响性能)。") | ||
| 945 | + | ||
| 946 | + | ||
| 947 | +# ---------------------------------------------------------------- 入口 | ||
| 948 | + | ||
| 949 | + | ||
| 950 | +def create_parser() -> argparse.ArgumentParser: | ||
| 951 | + parser = argparse.ArgumentParser( | ||
| 952 | + description="GE 多流自动寻优驱动", | ||
| 953 | + formatter_class=argparse.ArgumentDefaultsHelpFormatter, | ||
| 954 | + ) | ||
| 955 | + parser.add_argument( | ||
| 956 | + "--mode", | ||
| 957 | + choices=("online", "offline"), | ||
| 958 | + default="online", | ||
| 959 | + help="online:本机执行被测命令;offline:编译 OM 后送目标机执行", | ||
| 960 | + ) | ||
| 961 | + parser.add_argument("--run-command", help="online 必填:被测命令,整体加引号") | ||
| 962 | + parser.add_argument( | ||
| 963 | + "--compile-command", | ||
| 964 | + help="offline 必填:编译 OM 的命令,用 {om} 或 {om_prefix} 占位输出路径", | ||
| 965 | + ) | ||
| 966 | + parser.add_argument("--target", help="offline 必填:目标机配置 JSON 路径") | ||
| 967 | + parser.add_argument("--om-dir", help="offline:OM 产物目录,默认 <output-dir>/om") | ||
| 968 | + parser.add_argument( | ||
| 969 | + "--strategies", | ||
| 970 | + default="LoadBalance,MainStream", | ||
| 971 | + help="候选策略,逗号分隔,可选 {}".format("/".join(STRATEGIES)), | ||
| 972 | + ) | ||
| 973 | + parser.add_argument( | ||
| 974 | + "--streams", default="2,4,8", help="候选流数,逗号分隔,取值 [1,64]" | ||
| 975 | + ) | ||
| 976 | + parser.add_argument( | ||
| 977 | + "--configs", help="直接指定候选(如 default,LoadBalance:4),指定后忽略矩阵参数" | ||
| 978 | + ) | ||
| 979 | + parser.add_argument("--repeat", type=int, default=3, help="每个候选重复执行的轮数") | ||
| 980 | + parser.add_argument( | ||
| 981 | + "--drop-first", type=int, default=1, help="丢弃前若干个 STEP(预热)" | ||
| 982 | + ) | ||
| 983 | + parser.add_argument("--min-steps", type=int, default=5, help="单轮有效 STEP 数下限") | ||
| 984 | + parser.add_argument( | ||
| 985 | + "--main-graph", help="指定主执行对象:session_id:graph_id 或 model:model_id" | ||
| 986 | + ) | ||
| 987 | + parser.add_argument( | ||
| 988 | + "--timeout", type=int, default=1800, help="单轮超时秒数,0 表示不限制" | ||
| 989 | + ) | ||
| 990 | + parser.add_argument( | ||
| 991 | + "--output-dir", | ||
| 992 | + default="./ge_ms_autotune_output", | ||
| 993 | + help="结果目录,需不存在或为空", | ||
| 994 | + ) | ||
| 995 | + return parser | ||
| 996 | + | ||
| 997 | + | ||
| 998 | +def prepare_args(argv: Optional[Sequence[str]]) -> argparse.Namespace: | ||
| 999 | + args = create_parser().parse_args(argv) | ||
| 1000 | + if args.repeat < 1 or args.drop_first < 0 or args.min_steps < 1: | ||
| 1001 | + raise AutotuneError("--repeat/--min-steps 需大于 0,--drop-first 不能为负。") | ||
| 1002 | + prepare_mode_args(args) | ||
| 1003 | + args.main_graph = parse_main_graph(args.main_graph) | ||
| 1004 | + args.output_dir = Path(args.output_dir).expanduser().resolve() | ||
| 1005 | + if args.output_dir.exists() and any(args.output_dir.iterdir()): | ||
| 1006 | + raise AutotuneError( | ||
| 1007 | + "结果目录非空,请换一个 --output-dir:{}".format(args.output_dir) | ||
| 1008 | + ) | ||
| 1009 | + if args.mode == "offline" and args.om_dir is None: | ||
| 1010 | + args.om_dir = args.output_dir / "om" | ||
| 1011 | + return args | ||
| 1012 | + | ||
| 1013 | + | ||
| 1014 | +def prepare_mode_args(args: argparse.Namespace) -> None: | ||
| 1015 | + """在线取 --run-command,离线取目标机配置里的 run_command。""" | ||
| 1016 | + if args.mode == "online": | ||
| 1017 | + if args.compile_command or args.target or args.om_dir: | ||
| 1018 | + raise AutotuneError( | ||
| 1019 | + "--compile-command/--target/--om-dir 仅在 --mode offline 下有效。" | ||
| 1020 | + ) | ||
| 1021 | + args.argv = shlex.split(args.run_command or "") | ||
| 1022 | + if not args.argv: | ||
| 1023 | + raise AutotuneError("--mode online 需要 --run-command。") | ||
| 1024 | + args.command_text = " ".join(shlex.quote(item) for item in args.argv) | ||
| 1025 | + return | ||
| 1026 | + if args.run_command: | ||
| 1027 | + raise AutotuneError( | ||
| 1028 | + "--mode offline 的执行命令由目标机配置的 run_command 给出,不要用 --run-command。" | ||
| 1029 | + ) | ||
| 1030 | + if not args.compile_command or not args.target: | ||
| 1031 | + raise AutotuneError("--mode offline 需要 --compile-command 与 --target。") | ||
| 1032 | + args.compile_argv = shlex.split(args.compile_command) | ||
| 1033 | + if not any("{om}" in item or "{om_prefix}" in item for item in args.compile_argv): | ||
| 1034 | + raise AutotuneError("--compile-command 必须包含 {om} 或 {om_prefix} 占位符。") | ||
| 1035 | + args.argv = [] | ||
| 1036 | + args.target = load_target(args.target) | ||
| 1037 | + args.command_text = args.target.run_command | ||
| 1038 | + if args.om_dir: | ||
| 1039 | + args.om_dir = Path(args.om_dir).expanduser().resolve() | ||
| 1040 | + | ||
| 1041 | + | ||
| 1042 | +def execute_trials( | ||
| 1043 | + configs: Sequence[str], args: argparse.Namespace | ||
| 1044 | +) -> List[TrialResult]: | ||
| 1045 | + """按候选 × 轮次执行;离线模式先编译并上传全部候选 OM,结束后清理远端。""" | ||
| 1046 | + remote_oms: Dict[str, str] = {} | ||
| 1047 | + if args.mode == "offline": | ||
| 1048 | + remote_oms = upload_candidates(compile_candidates(configs, args), args) | ||
| 1049 | + print() | ||
| 1050 | + results: List[TrialResult] = [] | ||
| 1051 | + index = 0 | ||
| 1052 | + try: | ||
| 1053 | + for repeat in range(1, args.repeat + 1): | ||
| 1054 | + for config in configs: | ||
| 1055 | + if args.mode == "offline": | ||
| 1056 | + results.append( | ||
| 1057 | + run_offline_trial( | ||
| 1058 | + config, repeat, index, args, remote_oms[config] | ||
| 1059 | + ) | ||
| 1060 | + ) | ||
| 1061 | + else: | ||
| 1062 | + results.append(run_trial(config, repeat, index, args)) | ||
| 1063 | + index += 1 | ||
| 1064 | + finally: | ||
| 1065 | + if args.mode == "offline": | ||
| 1066 | + cleanup_remote(args) | ||
| 1067 | + return results | ||
| 1068 | + | ||
| 1069 | + | ||
| 1070 | +def run(argv: Optional[Sequence[str]] = None) -> int: | ||
| 1071 | + args = prepare_args(argv) | ||
| 1072 | + configs = build_configs(args.configs, args.strategies, args.streams) | ||
| 1073 | + args.output_dir.mkdir(parents=True, exist_ok=True) | ||
| 1074 | + print( | ||
| 1075 | + "候选配置({} 个 × {} 轮,{} 模式):{}".format( | ||
| 1076 | + len(configs), args.repeat, args.mode, ", ".join(configs) | ||
| 1077 | + ) | ||
| 1078 | + ) | ||
| 1079 | + print("结果目录:{}\n".format(args.output_dir)) | ||
| 1080 | + results = execute_trials(configs, args) | ||
| 1081 | + summaries = [ | ||
| 1082 | + summarize(config, [item for item in results if item.config == config]) | ||
| 1083 | + for config in configs | ||
| 1084 | + ] | ||
| 1085 | + apply_speedup(summaries) | ||
| 1086 | + summaries = rank(summaries) | ||
| 1087 | + write_summaries(args.output_dir, summaries, results, args) | ||
| 1088 | + print_report(summaries, args) | ||
| 1089 | + print("\n明细:{}/summary.csv、summary.json".format(args.output_dir)) | ||
| 1090 | + return 0 if any(item.median_us is not None for item in summaries) else 1 | ||
| 1091 | + | ||
| 1092 | + | ||
| 1093 | +def main() -> None: | ||
| 1094 | + try: | ||
| 1095 | + sys.exit(run()) | ||
| 1096 | + except AutotuneError as error: | ||
| 1097 | + sys.stdout.flush() # 管道下 stdout 带缓冲,先冲掉再打错误,避免顺序错乱 | ||
| 1098 | + print("[错误] {}".format(error), file=sys.stderr) | ||
| 1099 | + sys.exit(2) | ||
| 1100 | + except KeyboardInterrupt: | ||
| 1101 | + sys.stdout.flush() | ||
| 1102 | + print("\n[中断] 寻优已终止。", file=sys.stderr) | ||
| 1103 | + sys.exit(130) | ||
| 1104 | + | ||
| 1105 | + | ||
| 1106 | +if __name__ == "__main__": | ||
| 1107 | + main() | ||
| @@ -0,0 +1,129 @@ | |||
| 1 | +#!/usr/bin/env python3 | ||
| 2 | +# ---------------------------------------------------------------------------- | ||
| 3 | +# Copyright (c) 2026 Huawei Technologies Co., Ltd. | ||
| 4 | +# This program is free software, you can redistribute it and/or modify it under the terms and conditions of | ||
| 5 | +# CANN Open Software License Agreement Version 2.0 (the "License"). | ||
| 6 | +# Please refer to the License for details. You may not use this file except in compliance with the License. | ||
| 7 | +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, | ||
| 8 | +# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. | ||
| 9 | +# See LICENSE in the root of the software repository for the full text of the License. | ||
| 10 | +# ---------------------------------------------------------------------------- | ||
| 11 | + | ||
| 12 | +"""多流寻优的最小被测样例:构造多分支静态图并通过 Session 反复执行。 | ||
| 13 | + | ||
| 14 | +图由四条互不依赖的 pointwise 分支组成,多流并行时分支可分派到不同流上,因此不同 | ||
| 15 | +候选配置的端到端耗时差异可被观测到。每轮 run_graph 由 GE 输出一条 STEP 日志, | ||
| 16 | +供 ge_ms_autotune.py 统计。第 0 步为预热,对应寻优工具默认的 --drop-first=1。 | ||
| 17 | + | ||
| 18 | +本文件只是一个参照实现,实际寻优应把 --run-command 指向真实业务命令。 | ||
| 19 | +""" | ||
| 20 | + | ||
| 21 | +import argparse | ||
| 22 | +import os | ||
| 23 | +import sys | ||
| 24 | + | ||
| 25 | +DEFAULT_DIM = 512 | ||
| 26 | +BRANCH_COUNT = 4 | ||
| 27 | +BRANCH_DEPTH = 6 | ||
| 28 | +INPUT_COUNT = BRANCH_COUNT * 2 | ||
| 29 | + | ||
| 30 | + | ||
| 31 | +def positive_int(value: str) -> int: | ||
| 32 | + parsed = int(value) | ||
| 33 | + if parsed <= 0: | ||
| 34 | + raise argparse.ArgumentTypeError("must be greater than 0") | ||
| 35 | + return parsed | ||
| 36 | + | ||
| 37 | + | ||
| 38 | +def build_graph(dim: int): | ||
| 39 | + """构造 BRANCH_COUNT 条独立分支、同样数量输出的静态图。""" | ||
| 40 | + from ge.es.graph_builder import GraphBuilder | ||
| 41 | + from ge.graph.types import DataType | ||
| 42 | + | ||
| 43 | + builder = GraphBuilder("MultiStreamAutotuneGraph") | ||
| 44 | + inputs = [ | ||
| 45 | + builder.create_input( | ||
| 46 | + index=index, | ||
| 47 | + name="input_{}".format(index), | ||
| 48 | + data_type=DataType.DT_FLOAT, | ||
| 49 | + shape=[1, dim, dim], | ||
| 50 | + ) | ||
| 51 | + for index in range(INPUT_COUNT) | ||
| 52 | + ] | ||
| 53 | + for branch_index in range(BRANCH_COUNT): | ||
| 54 | + left = inputs[branch_index * 2] | ||
| 55 | + right = inputs[branch_index * 2 + 1] | ||
| 56 | + branch = left + right | ||
| 57 | + for _ in range(BRANCH_DEPTH): | ||
| 58 | + branch = branch * left + right | ||
| 59 | + builder.set_graph_output(branch, branch_index) | ||
| 60 | + return builder.build_and_reset() | ||
| 61 | + | ||
| 62 | + | ||
| 63 | +def create_inputs(dim: int): | ||
| 64 | + from ge.graph import Tensor | ||
| 65 | + from ge.graph.types import DataType, Format | ||
| 66 | + | ||
| 67 | + element_count = dim * dim | ||
| 68 | + return [ | ||
| 69 | + Tensor( | ||
| 70 | + [float(index + 1) / float(INPUT_COUNT)] * element_count, | ||
| 71 | + None, | ||
| 72 | + DataType.DT_FLOAT, | ||
| 73 | + Format.FORMAT_ND, | ||
| 74 | + [1, dim, dim], | ||
| 75 | + ) | ||
| 76 | + for index in range(INPUT_COUNT) | ||
| 77 | + ] | ||
| 78 | + | ||
| 79 | + | ||
| 80 | +def execute(args: argparse.Namespace) -> int: | ||
| 81 | + from ge.ge_global import GeApi | ||
| 82 | + from ge.session import Session | ||
| 83 | + | ||
| 84 | + graph_id = 1 | ||
| 85 | + session = None | ||
| 86 | + initialized = False | ||
| 87 | + try: | ||
| 88 | + GeApi.ge_initialize( | ||
| 89 | + {"ge.exec.deviceId": str(args.device), "ge.graphRunMode": "0"} | ||
| 90 | + ) | ||
| 91 | + initialized = True | ||
| 92 | + session = Session() | ||
| 93 | + session.add_graph(graph_id, build_graph(args.dim)) | ||
| 94 | + inputs = create_inputs(args.dim) | ||
| 95 | + outputs = session.run_graph(graph_id, inputs) # STEP 0:预热 | ||
| 96 | + for _ in range(args.steps): | ||
| 97 | + outputs = session.run_graph(graph_id, inputs) | ||
| 98 | + print( | ||
| 99 | + "[Info] 样例执行完成:device={}, steps={}, outputs={}".format( | ||
| 100 | + args.device, args.steps, [output.get_shape() for output in outputs] | ||
| 101 | + ) | ||
| 102 | + ) | ||
| 103 | + return 0 | ||
| 104 | + except Exception as error: # noqa: BLE001 - 样例进程以退出码反馈失败即可 | ||
| 105 | + print("[Error] 样例执行失败:{}".format(error), file=sys.stderr) | ||
| 106 | + return 1 | ||
| 107 | + finally: | ||
| 108 | + # Session 必须先于 GE 去初始化释放。 | ||
| 109 | + session = None | ||
| 110 | + if initialized: | ||
| 111 | + GeApi.ge_finalize() | ||
| 112 | + | ||
| 113 | + | ||
| 114 | +def main() -> int: | ||
| 115 | + parser = argparse.ArgumentParser(description=__doc__) | ||
| 116 | + parser.add_argument( | ||
| 117 | + "--steps", type=positive_int, default=12, help="预热后的执行轮数" | ||
| 118 | + ) | ||
| 119 | + parser.add_argument( | ||
| 120 | + "--dim", type=positive_int, default=DEFAULT_DIM, help="单边矩阵规模" | ||
| 121 | + ) | ||
| 122 | + parser.add_argument( | ||
| 123 | + "--device", type=int, default=int(os.environ.get("ASCEND_DEVICE_ID", "0")) | ||
| 124 | + ) | ||
| 125 | + return execute(parser.parse_args()) | ||
| 126 | + | ||
| 127 | + | ||
| 128 | +if __name__ == "__main__": | ||
| 129 | + sys.exit(main()) | ||
| @@ -751,6 +751,8 @@ const std::string ATTR_MODEL_NOTIFY_TYPES = "notify_types"; | |||
| 751 | 751 | ||
| 752 | const std::string ATTR_MODEL_HUGE_STREAM_LIST = "huge_stream_list"; | 752 | const std::string ATTR_MODEL_HUGE_STREAM_LIST = "huge_stream_list"; |
| 753 | 753 | ||
| 754 | +const std::string ATTR_MODEL_AUTO_MULTISTREAM_TUNING_MODE = "_auto_multistream_tuning_mode"; | ||
| 755 | + | ||
| 754 | const std::string ATTR_MODEL_LABEL_NUM = "label_num"; | 756 | const std::string ATTR_MODEL_LABEL_NUM = "label_num"; |
| 755 | 757 | ||
| 756 | const std::string ATTR_MODEL_MEMORY_SIZE = "memory_size"; | 758 | const std::string ATTR_MODEL_MEMORY_SIZE = "memory_size"; |
| @@ -33,11 +33,52 @@ const std::map<CustomPassStage, std::string> kCustomPassStageToStringMap = { | |||
| 33 | {CustomPassStage::kInvalid, "InvalidStage"}}; | 33 | {CustomPassStage::kInvalid, "InvalidStage"}}; |
| 34 | 34 | ||
| 35 | namespace { | 35 | namespace { |
| 36 | +constexpr const char_t *const kMiniDagStreamPass = "MiniDAGStreamPass"; | ||
| 37 | +constexpr const char_t *const kAutoMultistreamParallelModeOption = "ge.autoMultistreamParallelMode"; | ||
| 38 | + | ||
| 36 | std::string CustomPassStageToString(CustomPassStage stage) { | 39 | std::string CustomPassStageToString(CustomPassStage stage) { |
| 37 | GE_ASSERT_TRUE(stage <= CustomPassStage::kInvalid); | 40 | GE_ASSERT_TRUE(stage <= CustomPassStage::kInvalid); |
| 38 | return kCustomPassStageToStringMap.find(stage)->second; | 41 | return kCustomPassStageToStringMap.find(stage)->second; |
| 39 | } | 42 | } |
| 40 | 43 | ||
| 44 | +bool GetAutoMultistreamModeFromGraph(const GraphPtr &graph, std::string &multi_stream_mode) { | ||
| 45 | + if (graph == nullptr) { | ||
| 46 | + return false; | ||
| 47 | + } | ||
| 48 | + AttrValue attr_value; | ||
| 49 | + if (graph->GetAttr(AscendString(kAutoMultistreamParallelModeOption), attr_value) != GRAPH_SUCCESS) { | ||
| 50 | + return false; | ||
| 51 | + } | ||
| 52 | + AscendString value; | ||
| 53 | + if (attr_value.GetAttrValue(value) != GRAPH_SUCCESS) { | ||
| 54 | + return false; | ||
| 55 | + } | ||
| 56 | + const char_t *const value_str = value.GetString(); | ||
| 57 | + multi_stream_mode = (value_str == nullptr) ? "" : value_str; | ||
| 58 | + return true; | ||
| 59 | +} | ||
| 60 | + | ||
| 61 | +bool ShouldSkipMiniDagStreamPass(const PassRegistrationData ®_data, const GraphPtr &graph) { | ||
| 62 | + if (reg_data.GetPassName() != kMiniDagStreamPass) { | ||
| 63 | + return false; | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + std::string multi_stream_mode; | ||
| 67 | + const bool from_graph = GetAutoMultistreamModeFromGraph(graph, multi_stream_mode); | ||
| 68 | + if ((!from_graph) && | ||
| 69 | + (GetContext().GetOption(kAutoMultistreamParallelModeOption, multi_stream_mode) != GRAPH_SUCCESS)) { | ||
| 70 | + GELOGI("MiniDAGStreamPass skipped: auto multistream parallel mode not set."); | ||
| 71 | + return true; | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + const bool should_skip = | ||
| 75 | + multi_stream_mode.empty() || (multi_stream_mode == "cv") || (from_graph && (multi_stream_mode == "default")); | ||
| 76 | + if (should_skip) { | ||
| 77 | + GELOGI("MiniDAGStreamPass skipped for auto multistream parallel mode %s.", multi_stream_mode.c_str()); | ||
| 78 | + } | ||
| 79 | + return should_skip; | ||
| 80 | +} | ||
| 81 | + | ||
| 41 | Status RunAllocateStreamPass(const PassRegistrationData ®_data, const GraphPtr &graph, | 82 | Status RunAllocateStreamPass(const PassRegistrationData ®_data, const GraphPtr &graph, |
| 42 | CustomPassContext &custom_pass_context) { | 83 | CustomPassContext &custom_pass_context) { |
| 43 | GE_ASSERT_NOTNULL(graph); | 84 | GE_ASSERT_NOTNULL(graph); |
| @@ -54,18 +95,8 @@ Status RunAllocateStreamPass(const PassRegistrationData ®_data, const GraphPt | |||
| 54 | return FAILED; | 95 | return FAILED; |
| 55 | } | 96 | } |
| 56 | 97 | ||
| 57 | - // DAG 模块开关判断:仅判断 option 是否存在,具体解析和错误处理由 RunMiniDAGStreamPass 负责 | 98 | + if (ShouldSkipMiniDagStreamPass(reg_data, graph)) { |
| 58 | - if (reg_data.GetPassName() == "MiniDAGStreamPass") { | 99 | + return SUCCESS; |
| 59 | - std::string multi_stream_mode; | ||
| 60 | - if (GetContext().GetOption("ge.autoMultistreamParallelMode", multi_stream_mode) != GRAPH_SUCCESS || | ||
| 61 | - multi_stream_mode.empty()) { | ||
| 62 | - GELOGI("MiniDAGStreamPass skipped: ge.autoMultistreamParallelMode not set."); | ||
| 63 | - return SUCCESS; | ||
| 64 | - } | ||
| 65 | - if (multi_stream_mode == "cv") { | ||
| 66 | - GELOGI("MiniDAGStreamPass skipped: MiniDAGStreamPass not handle cv parallel."); | ||
| 67 | - return SUCCESS; | ||
| 68 | - } | ||
| 69 | } | 100 | } |
| 70 | 101 | ||
| 71 | const auto compute_graph = GraphUtilsEx::GetComputeGraph(*graph); | 102 | const auto compute_graph = GraphUtilsEx::GetComputeGraph(*graph); |
| @@ -244,6 +244,9 @@ class VISIBILITY_EXPORT ModelV2Executor { | |||
| 244 | ExecutorSubscribersScheduler subscribers_; | 244 | ExecutorSubscribersScheduler subscribers_; |
| 245 | ExecutorState state_ = ExecutorState::kInit; | 245 | ExecutorState state_ = ExecutorState::kInit; |
| 246 | std::string file_constant_weight_dir_; | 246 | std::string file_constant_weight_dir_; |
| 247 | + // 自动多流寻优标识,空表示不打点;本执行器无 model_id,由打点模块分配 | ||
| 248 | + std::string auto_multistream_tuning_mode_; | ||
| 249 | + uint32_t auto_multistream_tuning_id_ = 0U; | ||
| 247 | /* | 250 | /* |
| 248 | * 背景:对于aipp离线推理场景下,acl需要获取编译时期很多aipp的相关信息,rt2场景下需要适配 | 251 | * 背景:对于aipp离线推理场景下,acl需要获取编译时期很多aipp的相关信息,rt2场景下需要适配 |
| 249 | * 临时规避方案:因为是客户问题,时间比较紧,所以当前简单仿照静态shape下获取aipp的逻辑, | 252 | * 临时规避方案:因为是客户问题,时间比较紧,所以当前简单仿照静态shape下获取aipp的逻辑, |
| @@ -732,6 +732,8 @@ GE_FUNC_DEV_VISIBILITY GE_FUNC_HOST_VISIBILITY extern const std::string ATTR_MOD | |||
| 732 | 732 | ||
| 733 | GE_FUNC_DEV_VISIBILITY GE_FUNC_HOST_VISIBILITY extern const std::string ATTR_MODEL_HUGE_STREAM_LIST; | 733 | GE_FUNC_DEV_VISIBILITY GE_FUNC_HOST_VISIBILITY extern const std::string ATTR_MODEL_HUGE_STREAM_LIST; |
| 734 | 734 | ||
| 735 | +GE_FUNC_DEV_VISIBILITY GE_FUNC_HOST_VISIBILITY extern const std::string ATTR_MODEL_AUTO_MULTISTREAM_TUNING_MODE; | ||
| 736 | + | ||
| 735 | GE_FUNC_DEV_VISIBILITY GE_FUNC_HOST_VISIBILITY extern const std::string ATTR_MODEL_LABEL_NUM; | 737 | GE_FUNC_DEV_VISIBILITY GE_FUNC_HOST_VISIBILITY extern const std::string ATTR_MODEL_LABEL_NUM; |
| 736 | 738 | ||
| 737 | GE_FUNC_DEV_VISIBILITY GE_FUNC_HOST_VISIBILITY extern const std::string ATTR_MODEL_MEMORY_SIZE; | 739 | GE_FUNC_DEV_VISIBILITY GE_FUNC_HOST_VISIBILITY extern const std::string ATTR_MODEL_MEMORY_SIZE; |
| @@ -25,6 +25,8 @@ set(EXECUTOR_SRC_LIST | |||
| 25 | set(DAVINCI_SRC_LIST | 25 | set(DAVINCI_SRC_LIST |
| 26 | common/runtime/model_rt_var_manager.cc | 26 | common/runtime/model_rt_var_manager.cc |
| 27 | common/runtime/rt_session.cc | 27 | common/runtime/rt_session.cc |
| 28 | + common/multi_stream_tuning/model_tuning_config.cc | ||
| 29 | + common/multi_stream_tuning/step_recorder.cc | ||
| 28 | common/dump/opdebug_register.cc | 30 | common/dump/opdebug_register.cc |
| 29 | common/dump/data_dumper.cc | 31 | common/dump/data_dumper.cc |
| 30 | common/runtime_api_wrapper.cc | 32 | common/runtime_api_wrapper.cc |
| @@ -361,6 +363,7 @@ target_compile_options(davinci_executor PRIVATE ${AIR_COMMON_DYNAMIC_COMPILE_OPT | |||
| 361 | target_compile_definitions(davinci_executor PRIVATE | 363 | target_compile_definitions(davinci_executor PRIVATE |
| 362 | PROTOBUF_INLINE_NOT_IN_HEADERS=0 | 364 | PROTOBUF_INLINE_NOT_IN_HEADERS=0 |
| 363 | google=ascend_private | 365 | google=ascend_private |
| 366 | + FUNC_VISIBILITY | ||
| 364 | $<$<STREQUAL:${TARGET_SYSTEM_NAME},Windows>:SECUREC_USING_STD_SECURE_LIB=0 NOMINMAX> | 367 | $<$<STREQUAL:${TARGET_SYSTEM_NAME},Windows>:SECUREC_USING_STD_SECURE_LIB=0 NOMINMAX> |
| 365 | ) | 368 | ) |
| 366 | 369 | ||
| @@ -0,0 +1,48 @@ | |||
| 1 | +/** | ||
| 2 | + * Copyright (c) 2026 Huawei Technologies Co., Ltd. | ||
| 3 | + * This program is free software; you can redistribute it and/or modify it under the terms and conditions of | ||
| 4 | + * CANN Open Software License Agreement Version 2.0 (the "License"). | ||
| 5 | + * Please refer to the License for details. You may not use this file except in compliance with the License. | ||
| 6 | + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, | ||
| 7 | + * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. | ||
| 8 | + * See LICENSE in the root of the software repository for the full text of the License. | ||
| 9 | + */ | ||
| 10 | + | ||
| 11 | + | ||
| 12 | + | ||
| 13 | + | ||
| 14 | + | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | +namespace ge { | ||
| 19 | +namespace multistream_tune { | ||
| 20 | +bool GetTuningMode(const GeModelPtr &model, std::string &mode) { | ||
| 21 | + mode.clear(); | ||
| 22 | + return (model != nullptr) && AttrUtils::GetStr(model, ATTR_MODEL_AUTO_MULTISTREAM_TUNING_MODE, mode) && | ||
| 23 | + (!mode.empty()); | ||
| 24 | +} | ||
| 25 | + | ||
| 26 | +bool GetTuningMode(const GeRootModelPtr &root_model, std::string &mode) { | ||
| 27 | + mode.clear(); | ||
| 28 | + if (root_model == nullptr) { | ||
| 29 | + return false; | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + const auto &models = root_model->GetSubgraphInstanceNameToModel(); | ||
| 33 | + const auto &root_graph = root_model->GetRootGraph(); | ||
| 34 | + if (root_graph != nullptr) { | ||
| 35 | + const auto root_model_iter = models.find(root_graph->GetName()); | ||
| 36 | + if ((root_model_iter != models.end()) && GetTuningMode(root_model_iter->second, mode)) { | ||
| 37 | + return true; | ||
| 38 | + } | ||
| 39 | + } | ||
| 40 | + for (const auto &model : models) { | ||
| 41 | + if (GetTuningMode(model.second, mode)) { | ||
| 42 | + return true; | ||
| 43 | + } | ||
| 44 | + } | ||
| 45 | + return false; | ||
| 46 | +} | ||
| 47 | +} // namespace multistream_tune | ||
| 48 | +} // namespace ge | ||
| @@ -0,0 +1,29 @@ | |||
| 1 | +/** | ||
| 2 | + * Copyright (c) 2026 Huawei Technologies Co., Ltd. | ||
| 3 | + * This program is free software; you can redistribute it and/or modify it under the terms and conditions of | ||
| 4 | + * CANN Open Software License Agreement Version 2.0 (the "License"). | ||
| 5 | + * Please refer to the License for details. You may not use this file except in compliance with the License. | ||
| 6 | + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, | ||
| 7 | + * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. | ||
| 8 | + * See LICENSE in the root of the software repository for the full text of the License. | ||
| 9 | + */ | ||
| 10 | + | ||
| 11 | + | ||
| 12 | + | ||
| 13 | + | ||
| 14 | + | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | +namespace ge { | ||
| 20 | +class GeModel; | ||
| 21 | +class GeRootModel; | ||
| 22 | + | ||
| 23 | +namespace multistream_tune { | ||
| 24 | +VISIBILITY_EXPORT bool GetTuningMode(const std::shared_ptr<GeModel> &model, std::string &mode); | ||
| 25 | +VISIBILITY_EXPORT bool GetTuningMode(const std::shared_ptr<GeRootModel> &root_model, std::string &mode); | ||
| 26 | +} // namespace multistream_tune | ||
| 27 | +} // namespace ge | ||
| 28 | + | ||
| 29 | + | ||
| @@ -0,0 +1,143 @@ | |||
| 1 | +/** | ||
| 2 | + * Copyright (c) 2026 Huawei Technologies Co., Ltd. | ||
| 3 | + * This program is free software; you can redistribute it and/or modify it under the terms and conditions of | ||
| 4 | + * CANN Open Software License Agreement Version 2.0 (the "License"). | ||
| 5 | + * Please refer to the License for details. You may not use this file except in compliance with the License. | ||
| 6 | + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, | ||
| 7 | + * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. | ||
| 8 | + * See LICENSE in the root of the software repository for the full text of the License. | ||
| 9 | + */ | ||
| 10 | + | ||
| 11 | + | ||
| 12 | + | ||
| 13 | + | ||
| 14 | + | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + | ||
| 24 | +namespace ge { | ||
| 25 | +GE_FUNC_VISIBILITY uint64_t GetCurrentTimestamp(); | ||
| 26 | + | ||
| 27 | +namespace multistream_tune { | ||
| 28 | +namespace { | ||
| 29 | +constexpr size_t kMaxModeLength = 128U; | ||
| 30 | +// 未显式停表时的收尾返回值,寻优工具据此判该步无效 | ||
| 31 | +constexpr uint32_t kStepAbortRet = 0xFFFFFFFFU; | ||
| 32 | +// 执行成功的返回值,ge::SUCCESS 与 gert 的 GRAPH_SUCCESS 同为 0 | ||
| 33 | +constexpr uint32_t kStepSuccessRet = 0U; | ||
| 34 | +// RT2.0 执行器无 model_id,其标识从高位区间分配,避免与 acl 侧递增的 model_id 混淆 | ||
| 35 | +constexpr uint32_t kExecutionIdBase = 0x7F000000U; | ||
| 36 | + | ||
| 37 | +// 模式串直接进日志,去掉可能破坏 key=value 格式的字符 | ||
| 38 | +std::string SanitizeMode(const std::string &value) { | ||
| 39 | + std::string mode; | ||
| 40 | + for (size_t index = 0U; (index < kMaxModeLength) && (index < value.size()); ++index) { | ||
| 41 | + const auto ch = static_cast<unsigned char>(value[index]); | ||
| 42 | + if ((std::isalnum(ch) != 0) || (ch == ':') || (ch == '_') || (ch == '-') || (ch == '.')) { | ||
| 43 | + mode.push_back(static_cast<char>(ch)); | ||
| 44 | + } else { | ||
| 45 | + mode.push_back('_'); | ||
| 46 | + } | ||
| 47 | + } | ||
| 48 | + return mode; | ||
| 49 | +} | ||
| 50 | + | ||
| 51 | +std::mutex &StepCounterMutex() { | ||
| 52 | + static std::mutex mutex; | ||
| 53 | + return mutex; | ||
| 54 | +} | ||
| 55 | + | ||
| 56 | +std::map<uint32_t, uint64_t> &StepCounters() { | ||
| 57 | + static std::map<uint32_t, uint64_t> counters; | ||
| 58 | + return counters; | ||
| 59 | +} | ||
| 60 | + | ||
| 61 | +uint64_t NextStepId(const uint32_t execution_id) { | ||
| 62 | + const std::lock_guard<std::mutex> lock(StepCounterMutex()); | ||
| 63 | + auto &next_step_id = StepCounters()[execution_id]; | ||
| 64 | + const uint64_t step_id = next_step_id; | ||
| 65 | + if (next_step_id == std::numeric_limits<uint64_t>::max()) { | ||
| 66 | + GELOGW("[GE_MS_TUNE] Step id overflow, execution_id=%u.", execution_id); | ||
| 67 | + } else { | ||
| 68 | + ++next_step_id; | ||
| 69 | + } | ||
| 70 | + return step_id; | ||
| 71 | +} | ||
| 72 | + | ||
| 73 | +// 只统计最外层执行:同线程嵌套调用(如 Hybrid 动态 shape 内层复用 RT2.0 执行器)不重复计数。 | ||
| 74 | +// 跨线程不共享该深度,队列异步路径的 worker 线程因此仍能独立打点。 | ||
| 75 | +thread_local uint32_t g_step_depth = 0U; | ||
| 76 | +} // namespace | ||
| 77 | + | ||
| 78 | +uint32_t AllocateExecutionId() { | ||
| 79 | + static std::atomic<uint32_t> next_id{kExecutionIdBase}; | ||
| 80 | + return next_id.fetch_add(1U, std::memory_order_relaxed); | ||
| 81 | +} | ||
| 82 | + | ||
| 83 | +StepScope::StepScope(const char *const site, const std::string &mode, const uint32_t execution_id, void *const stream) | ||
| 84 | + : site_(site), execution_id_(execution_id), stream_(stream) { | ||
| 85 | + if (mode.empty() || (g_step_depth > 0U)) { | ||
| 86 | + return; | ||
| 87 | + } | ||
| 88 | + ++g_step_depth; | ||
| 89 | + active_ = true; | ||
| 90 | + mode_ = SanitizeMode(mode); | ||
| 91 | + step_id_ = NextStepId(execution_id); | ||
| 92 | + start_us_ = GetCurrentTimestamp(); | ||
| 93 | +} | ||
| 94 | + | ||
| 95 | +StepScope::~StepScope() { | ||
| 96 | + if (!active_) { | ||
| 97 | + return; | ||
| 98 | + } | ||
| 99 | + Record(kStepAbortRet); | ||
| 100 | + --g_step_depth; | ||
| 101 | +} | ||
| 102 | + | ||
| 103 | +void StepScope::Stop(const uint32_t ret) { | ||
| 104 | + Record(ret); | ||
| 105 | +} | ||
| 106 | + | ||
| 107 | +void StepScope::Record(const uint32_t ret) { | ||
| 108 | + if ((!active_) || stopped_) { | ||
| 109 | + return; | ||
| 110 | + } | ||
| 111 | + stopped_ = true; | ||
| 112 | + | ||
| 113 | + // 只有本步成功才同步:失败步骤的记录本就会被寻优工具丢弃,同步没有统计价值, | ||
| 114 | + // 反而可能在任务根本没下发时等待该 stream 上与本次执行无关的历史任务, | ||
| 115 | + // 或在调用方已 abort / 已超时返回后再次无谓等待。 | ||
| 116 | + const bool sync_stream = (ret == kStepSuccessRet) && (stream_ != nullptr); | ||
| 117 | + uint64_t sync_us = 0U; | ||
| 118 | + aclError sync_ret = ACL_SUCCESS; | ||
| 119 | + uint64_t end_us; | ||
| 120 | + if (!sync_stream) { | ||
| 121 | + end_us = GetCurrentTimestamp(); | ||
| 122 | + } else { | ||
| 123 | + // 沿用调用方配置的流同步超时,避免打点绕过既有的超时保护 | ||
| 124 | + const uint64_t sync_start_us = GetCurrentTimestamp(); | ||
| 125 | + sync_ret = aclrtSynchronizeStreamWithTimeout(static_cast<aclrtStream>(stream_), GetContext().StreamSyncTimeout()); | ||
| 126 | + end_us = GetCurrentTimestamp(); | ||
| 127 | + sync_us = (end_us >= sync_start_us) ? (end_us - sync_start_us) : 0U; | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + const bool interval_valid = (end_us >= start_us_); | ||
| 131 | + if (!interval_valid) { | ||
| 132 | + GELOGW("[GE_MS_TUNE] Invalid timestamp interval, api=%s, execution_id=%u, step=%" PRIu64 ", start_us=%" PRIu64 | ||
| 133 | + ", end_us=%" PRIu64 ".", | ||
| 134 | + site_, execution_id_, step_id_, start_us_, end_us); | ||
| 135 | + } | ||
| 136 | + const uint64_t cost_us = interval_valid ? (end_us - start_us_) : 0U; | ||
| 137 | + GEEVENT("[GE_MS_TUNE][STEP] api=%s mode=%s model_id=%u step=%" PRIu64 " start_us=%" PRIu64 " end_us=%" PRIu64 | ||
| 138 | + " cost_us=%" PRIu64 " sync_us=%" PRIu64 " ret=%u sync_ret=%d", | ||
| 139 | + site_, mode_.c_str(), execution_id_, step_id_, start_us_, end_us, cost_us, sync_us, ret, | ||
| 140 | + static_cast<int32_t>(sync_ret)); | ||
| 141 | +} | ||
| 142 | +} // namespace multistream_tune | ||
| 143 | +} // namespace ge | ||
| @@ -0,0 +1,81 @@ | |||
| 1 | +/** | ||
| 2 | + * Copyright (c) 2026 Huawei Technologies Co., Ltd. | ||
| 3 | + * This program is free software; you can redistribute it and/or modify it under the terms and conditions of | ||
| 4 | + * CANN Open Software License Agreement Version 2.0 (the "License"). | ||
| 5 | + * Please refer to the License for details. You may not use this file except in compliance with the License. | ||
| 6 | + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, | ||
| 7 | + * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. | ||
| 8 | + * See LICENSE in the root of the software repository for the full text of the License. | ||
| 9 | + */ | ||
| 10 | + | ||
| 11 | + | ||
| 12 | + | ||
| 13 | + | ||
| 14 | + | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | +namespace ge { | ||
| 20 | +namespace multistream_tune { | ||
| 21 | +/// 打点位置标识,输出到 STEP 日志的 api 字段,排障时用于判断本次执行落在哪条执行栈。 | ||
| 22 | +constexpr const char *kSiteNnExecute = "NnExecute"; | ||
| 23 | +constexpr const char *kSiteRun = "Run"; | ||
| 24 | +constexpr const char *kSiteModelV2Executor = "ModelV2Executor"; | ||
| 25 | + | ||
| 26 | +/// | ||
| 27 | +/// @brief 为无 model_id 的执行对象(RT2.0 执行器)分配打点用执行对象标识。 | ||
| 28 | +/// @return 进程内唯一的标识,取值区间与 model_id 不重叠。 | ||
| 29 | +/// | ||
| 30 | +GE_FUNC_VISIBILITY uint32_t AllocateExecutionId(); | ||
| 31 | + | ||
| 32 | +/// | ||
| 33 | +/// @brief 自动多流寻优单步耗时打点,RAII 兜底 + 显式停表。 | ||
| 34 | +/// | ||
| 35 | +/// 构造时若 mode 为空则退化为空对象,不取时间戳、不分配资源,生产态零开销; | ||
| 36 | +/// 同线程内已存在活跃实例时同样退化为空对象,保证只统计最外层(根图)执行。 | ||
| 37 | +/// 未显式 Stop 时由析构按失败收尾,函数中间的任意早退分支无需处理。 | ||
| 38 | +/// | ||
| 39 | +/// stream 同步**只在本步成功收尾时发生**(`Stop(0)`)。失败收尾——无论是显式 `Stop(非 0)` | ||
| 40 | +/// 还是析构兜底——一律不同步:此时任务可能压根没下发(校验/准备阶段就失败了), | ||
| 41 | +/// 也可能调用方已经超时并 Abort,该 stream 上的状态不可预期。 | ||
| 42 | +/// 这种记录本身按失败落盘、会被寻优工具丢弃,同步既无统计价值, | ||
| 43 | +/// 又会等待与本次执行无关的历史任务,或绕过调用方原有的超时/Abort 处理。 | ||
| 44 | +/// | ||
| 45 | +class GE_FUNC_VISIBILITY StepScope { | ||
| 46 | + public: | ||
| 47 | + /// | ||
| 48 | + /// @param [in] site 打点位置标识,取 kSite* 常量 | ||
| 49 | + /// @param [in] mode 模型携带的调测身份,空表示不打点 | ||
| 50 | + /// @param [in] execution_id 执行对象标识,V1 取 model_id,RT2.0 取 AllocateExecutionId 的返回值 | ||
| 51 | + /// @param [in] stream 非空且本步成功时,停表前同步该 stream(带调用方配置的超时), | ||
| 52 | + /// 用于界定异步下发的完成边界;须传本次实际下发的 stream。 | ||
| 53 | + /// 调用方已保证同步完成的位置传空,避免重复等待 | ||
| 54 | + /// | ||
| 55 | + StepScope(const char *site, const std::string &mode, uint32_t execution_id, void *stream = nullptr); | ||
| 56 | + ~StepScope(); | ||
| 57 | + | ||
| 58 | + StepScope(const StepScope &) = delete; | ||
| 59 | + StepScope(StepScope &&) = delete; | ||
| 60 | + StepScope &operator=(const StepScope &) = delete; | ||
| 61 | + StepScope &operator=(StepScope &&) = delete; | ||
| 62 | + | ||
| 63 | + /// @brief 停表并输出记录,幂等,重复调用忽略。 | ||
| 64 | + void Stop(uint32_t ret); | ||
| 65 | + | ||
| 66 | + private: | ||
| 67 | + void Record(uint32_t ret); | ||
| 68 | + | ||
| 69 | + const char *site_; | ||
| 70 | + std::string mode_; | ||
| 71 | + uint32_t execution_id_; | ||
| 72 | + void *stream_; | ||
| 73 | + uint64_t step_id_{0U}; | ||
| 74 | + uint64_t start_us_{0U}; | ||
| 75 | + bool active_{false}; | ||
| 76 | + bool stopped_{false}; | ||
| 77 | +}; | ||
| 78 | +} // namespace multistream_tune | ||
| 79 | +} // namespace ge | ||
| 80 | + | ||
| 81 | + | ||
| @@ -19,6 +19,8 @@ | |||
| 19 | 19 | ||
| 20 | 20 | ||
| 21 | 21 | ||
| 22 | + | ||
| 23 | + | ||
| 22 | 24 | ||
| 23 | 25 | ||
| 24 | 26 | ||
| @@ -499,6 +501,7 @@ void DavinciModel::Assign(const GeModelPtr &ge_model) { | |||
| 499 | GELOGW("Assign null to ge_model"); | 501 | GELOGW("Assign null to ge_model"); |
| 500 | } | 502 | } |
| 501 | ge_model_ = ge_model; | 503 | ge_model_ = ge_model; |
| 504 | + (void)multistream_tune::GetTuningMode(ge_model, auto_multistream_tuning_mode_); | ||
| 502 | } | 505 | } |
| 503 | 506 | ||
| 504 | /// | 507 | /// |
| @@ -5636,6 +5639,8 @@ void DavinciModel::Run() { | |||
| 5636 | } | 5639 | } |
| 5637 | GE_IF_BOOL_EXEC(is_prof_enabled, SetProfileTime(ModelProcStage::MODEL_PRE_PROC_END)); | 5640 | GE_IF_BOOL_EXEC(is_prof_enabled, SetProfileTime(ModelProcStage::MODEL_PRE_PROC_END)); |
| 5638 | GE_IF_BOOL_EXEC(is_prof_enabled, SetProfileTime(ModelProcStage::MODEL_INFER_START)); | 5641 | GE_IF_BOOL_EXEC(is_prof_enabled, SetProfileTime(ModelProcStage::MODEL_INFER_START)); |
| 5642 | + // 自动多流寻优打点:本函数内已无条件做流同步,无需再传 stream | ||
| 5643 | + multistream_tune::StepScope step(multistream_tune::kSiteRun, auto_multistream_tuning_mode_, model_id_); | ||
| 5639 | GE_TIMESTAMP_START(aclmdlRIExecuteAsync); | 5644 | GE_TIMESTAMP_START(aclmdlRIExecuteAsync); |
| 5640 | GELOGI("aclmdlRIExecuteAsync start, model id:%u.", model_id_); | 5645 | GELOGI("aclmdlRIExecuteAsync start, model id:%u.", model_id_); |
| 5641 | CANN_PROFILING_STEP_TRACE(model_id_, iterator_count_, 0U, rt_model_stream_); | 5646 | CANN_PROFILING_STEP_TRACE(model_id_, iterator_count_, 0U, rt_model_stream_); |
| @@ -5678,6 +5683,7 @@ void DavinciModel::Run() { | |||
| 5678 | model_abort ? "abort" : "normal"); | 5683 | model_abort ? "abort" : "normal"); |
| 5679 | GE_IF_BOOL_EXEC(is_first_execute_, GE_TIMESTAMP_EVENT_END(aclrtSynchronizeStreamWithTimeout, | 5684 | GE_IF_BOOL_EXEC(is_first_execute_, GE_TIMESTAMP_EVENT_END(aclrtSynchronizeStreamWithTimeout, |
| 5680 | "Wait for aclrtSynchronizeStreamWithTimeout")); | 5685 | "Wait for aclrtSynchronizeStreamWithTimeout")); |
| 5686 | + step.Stop(SUCCESS); | ||
| 5681 | GE_IF_BOOL_EXEC(is_prof_enabled, SetProfileTime(ModelProcStage::MODEL_INFER_END)); | 5687 | GE_IF_BOOL_EXEC(is_prof_enabled, SetProfileTime(ModelProcStage::MODEL_INFER_END)); |
| 5682 | GE_IF_BOOL_EXEC(is_prof_enabled, SetProfileTime(ModelProcStage::MODEL_AFTER_PROC_START)); | 5688 | GE_IF_BOOL_EXEC(is_prof_enabled, SetProfileTime(ModelProcStage::MODEL_AFTER_PROC_START)); |
| 5683 | GE_TIMESTAMP_START(ReturnResult); | 5689 | GE_TIMESTAMP_START(ReturnResult); |
| @@ -7731,6 +7737,9 @@ Status DavinciModel::NnExecute(aclrtStream const stream, const bool async_mode, | |||
| 7731 | GE_IF_BOOL_EXEC(is_dump_to_std_enable_, | 7737 | GE_IF_BOOL_EXEC(is_dump_to_std_enable_, |
| 7732 | davinci_model_stage_time_[kStageBeforeRtExecute] = std::chrono::system_clock::now()); | 7738 | davinci_model_stage_time_[kStageBeforeRtExecute] = std::chrono::system_clock::now()); |
| 7733 | GE_IF_BOOL_EXEC(is_prof_enabled, SetProfileTime(ModelProcStage::MODEL_PRE_PROC_END)); | 7739 | GE_IF_BOOL_EXEC(is_prof_enabled, SetProfileTime(ModelProcStage::MODEL_PRE_PROC_END)); |
| 7740 | + // 自动多流寻优打点:口径为「任务下发 -> 流同步完成」,不含 H2D / D2H 拷贝 | ||
| 7741 | + multistream_tune::StepScope step(multistream_tune::kSiteNnExecute, auto_multistream_tuning_mode_, model_id_, | ||
| 7742 | + rt_model_stream_); | ||
| 7734 | if (!task_list_.empty()) { | 7743 | if (!task_list_.empty()) { |
| 7735 | // used for debug resource manager | 7744 | // used for debug resource manager |
| 7736 | if (GetDumpProperties().IsDumpOpen() || GetDumpProperties().IsOpDebugOpen()) { | 7745 | if (GetDumpProperties().IsDumpOpen() || GetDumpProperties().IsOpDebugOpen()) { |
| @@ -7776,6 +7785,7 @@ Status DavinciModel::NnExecute(aclrtStream const stream, const bool async_mode, | |||
| 7776 | return FAILED; | 7785 | return FAILED; |
| 7777 | } | 7786 | } |
| 7778 | } | 7787 | } |
| 7788 | + step.Stop(SUCCESS); | ||
| 7779 | GE_IF_BOOL_EXEC(is_dump_to_std_enable_, | 7789 | GE_IF_BOOL_EXEC(is_dump_to_std_enable_, |
| 7780 | davinci_model_stage_time_[kStageAfterRtExecute] = std::chrono::system_clock::now()); | 7790 | davinci_model_stage_time_[kStageAfterRtExecute] = std::chrono::system_clock::now()); |
| 7781 | GE_IF_BOOL_EXEC(is_prof_enabled, SetProfileTime(ModelProcStage::MODEL_AFTER_PROC_START)); | 7791 | GE_IF_BOOL_EXEC(is_prof_enabled, SetProfileTime(ModelProcStage::MODEL_AFTER_PROC_START)); |
| @@ -7837,6 +7847,9 @@ Status DavinciModel::NnExecute(aclrtStream const stream, const bool async_mode, | |||
| 7837 | 7847 | ||
| 7838 | GE_IF_BOOL_EXEC(is_prof_enabled, SetProfileTime(ModelProcStage::MODEL_PRE_PROC_END)); | 7848 | GE_IF_BOOL_EXEC(is_prof_enabled, SetProfileTime(ModelProcStage::MODEL_PRE_PROC_END)); |
| 7839 | 7849 | ||
| 7850 | + // 自动多流寻优打点:口径为「任务下发 -> 流同步完成」,不含 H2D / D2H 拷贝 | ||
| 7851 | + multistream_tune::StepScope step(multistream_tune::kSiteNnExecute, auto_multistream_tuning_mode_, model_id_, | ||
| 7852 | + rt_model_stream_); | ||
| 7840 | if (!task_list_.empty()) { | 7853 | if (!task_list_.empty()) { |
| 7841 | // used for debug resource manager | 7854 | // used for debug resource manager |
| 7842 | if (GetDumpProperties().IsDumpOpen() || GetDumpProperties().IsOpDebugOpen()) { | 7855 | if (GetDumpProperties().IsDumpOpen() || GetDumpProperties().IsOpDebugOpen()) { |
| @@ -7886,6 +7899,7 @@ Status DavinciModel::NnExecute(aclrtStream const stream, const bool async_mode, | |||
| 7886 | } | 7899 | } |
| 7887 | } | 7900 | } |
| 7888 | 7901 | ||
| 7902 | + step.Stop(SUCCESS); | ||
| 7889 | GE_IF_BOOL_EXEC(is_prof_enabled, SetProfileTime(ModelProcStage::MODEL_AFTER_PROC_START)); | 7903 | GE_IF_BOOL_EXEC(is_prof_enabled, SetProfileTime(ModelProcStage::MODEL_AFTER_PROC_START)); |
| 7890 | output_data.index = input_data.index; | 7904 | output_data.index = input_data.index; |
| 7891 | output_data.model_id = model_id_; | 7905 | output_data.model_id = model_id_; |
| @@ -974,6 +974,10 @@ class DavinciModel { | |||
| 974 | return ge_model_->GetGraph(); | 974 | return ge_model_->GetGraph(); |
| 975 | } | 975 | } |
| 976 | 976 | ||
| 977 | + const std::string &GetAutoMultistreamTuningMode() const { | ||
| 978 | + return auto_multistream_tuning_mode_; | ||
| 979 | + } | ||
| 980 | + | ||
| 977 | uint64_t GetReportedProfCount() const { | 981 | uint64_t GetReportedProfCount() const { |
| 978 | return prof_count_.load(); | 982 | return prof_count_.load(); |
| 979 | } | 983 | } |
| @@ -1545,6 +1549,7 @@ class DavinciModel { | |||
| 1545 | 1549 | ||
| 1546 | uint32_t version_{0U}; | 1550 | uint32_t version_{0U}; |
| 1547 | GeModelPtr ge_model_; // release after DavinciModel::Init | 1551 | GeModelPtr ge_model_; // release after DavinciModel::Init |
| 1552 | + std::string auto_multistream_tuning_mode_; | ||
| 1548 | 1553 | ||
| 1549 | std::map<int64_t, OpDescPtr> op_list_; // release after DavinciModel::Init | 1554 | std::map<int64_t, OpDescPtr> op_list_; // release after DavinciModel::Init |
| 1550 | std::map<int64_t, std::shared_ptr<Operator>> operator_list_; | 1555 | std::map<int64_t, std::shared_ptr<Operator>> operator_list_; |
| @@ -28,6 +28,8 @@ | |||
| 28 | 28 | ||
| 29 | 29 | ||
| 30 | 30 | ||
| 31 | + | ||
| 32 | + | ||
| 31 | 33 | ||
| 32 | 34 | ||
| 33 | 35 | ||
| @@ -140,6 +142,9 @@ std::unique_ptr<ModelV2Executor> ModelV2ExecutorBuilder::Build(const ExecutorOpt | |||
| 140 | } | 142 | } |
| 141 | GE_TIMESTAMP_EVENT_END(BuildGraph, "ModelV2ExecutorBuilderBuild::BuildGraph"); | 143 | GE_TIMESTAMP_EVENT_END(BuildGraph, "ModelV2ExecutorBuilderBuild::BuildGraph"); |
| 142 | GE_ASSERT_NOTNULL(root_model_); | 144 | GE_ASSERT_NOTNULL(root_model_); |
| 145 | + if (ge::multistream_tune::GetTuningMode(root_model_, executor->auto_multistream_tuning_mode_)) { | ||
| 146 | + executor->auto_multistream_tuning_id_ = ge::multistream_tune::AllocateExecutionId(); | ||
| 147 | + } | ||
| 143 | executor->custom_op_registry_ = root_model_->GetCustomOpRegistry(); | 148 | executor->custom_op_registry_ = root_model_->GetCustomOpRegistry(); |
| 144 | 149 | ||
| 145 | ge::ComputeGraphPtr root_graph = root_model_->GetRootGraph(); | 150 | ge::ComputeGraphPtr root_graph = root_model_->GetRootGraph(); |
| @@ -17,6 +17,7 @@ | |||
| 17 | 17 | ||
| 18 | 18 | ||
| 19 | 19 | ||
| 20 | + | ||
| 20 | 21 | ||
| 21 | 22 | ||
| 22 | 23 | ||
| @@ -243,11 +244,16 @@ ge::graphStatus ModelV2Executor::Execute(const ModelExecuteArg &arg, Tensor **in | |||
| 243 | 244 | ||
| 244 | GE_RETURN_IF_ERROR(CheckIoReuseAddrs(inputs, input_num, outputs, output_num)); | 245 | GE_RETURN_IF_ERROR(CheckIoReuseAddrs(inputs, input_num, outputs, output_num)); |
| 245 | 246 | ||
| 247 | + ge::multistream_tune::StepScope step(ge::multistream_tune::kSiteModelV2Executor, auto_multistream_tuning_mode_, | ||
| 248 | + auto_multistream_tuning_id_, arg.stream); | ||
| 249 | + ge::graphStatus ret = ge::GRAPH_FAILED; | ||
| 246 | if (subscribers_.IsEnable()) { | 250 | if (subscribers_.IsEnable()) { |
| 247 | - return graph_executor.Execute(kMainExeGraph, &subscribers_.GetSubscriber(kMainExeGraph)); | 251 | + ret = graph_executor.Execute(kMainExeGraph, &subscribers_.GetSubscriber(kMainExeGraph)); |
| 248 | } else { | 252 | } else { |
| 249 | - return graph_executor.Execute(); | 253 | + ret = graph_executor.Execute(); |
| 250 | } | 254 | } |
| 255 | + step.Stop(static_cast<uint32_t>(ret)); | ||
| 256 | + return ret; | ||
| 251 | } | 257 | } |
| 252 | ge::graphStatus ModelV2Executor::ExecuteSync(Tensor **inputs, size_t input_num, Tensor **outputs, size_t output_num) { | 258 | ge::graphStatus ModelV2Executor::ExecuteSync(Tensor **inputs, size_t input_num, Tensor **outputs, size_t output_num) { |
| 253 | if (default_stream_ == nullptr) { | 259 | if (default_stream_ == nullptr) { |
| @@ -109,6 +109,7 @@ add_executable(graph_engine_test | |||
| 109 | "${AIR_CODE_DIR}/tests/ge/st/testcase/graph/build/dag/dag_stream_allocator_pass_test.cc" | 109 | "${AIR_CODE_DIR}/tests/ge/st/testcase/graph/build/dag/dag_stream_allocator_pass_test.cc" |
| 110 | "${AIR_CODE_DIR}/tests/ge/st/testcase/graph/build/dag/dag_weighted_stream_merger_public_st_test.cc" | 110 | "${AIR_CODE_DIR}/tests/ge/st/testcase/graph/build/dag/dag_weighted_stream_merger_public_st_test.cc" |
| 111 | "${AIR_CODE_DIR}/tests/ge/st/testcase/graph/ir/named_io_node_builder_test.cc" | 111 | "${AIR_CODE_DIR}/tests/ge/st/testcase/graph/ir/named_io_node_builder_test.cc" |
| 112 | + "${AIR_CODE_DIR}/tests/ge/st/testcase/test_multi_stream_tuning_step_recorder.cc" | ||
| 112 | ) | 113 | ) |
| 113 | 114 | ||
| 114 | add_dependencies(graph_engine_test | 115 | add_dependencies(graph_engine_test |
| @@ -155,6 +156,7 @@ target_link_libraries(graph_engine_test | |||
| 155 | ge_runner_v2 | 156 | ge_runner_v2 |
| 156 | om2_executor | 157 | om2_executor |
| 157 | ge_common_base | 158 | ge_common_base |
| 159 | + davinci_executor | ||
| 158 | custom_op_runtime | 160 | custom_op_runtime |
| 159 | gert | 161 | gert |
| 160 | cpu_compiler | 162 | cpu_compiler |
| @@ -26,6 +26,7 @@ | |||
| 26 | 26 | ||
| 27 | 27 | ||
| 28 | 28 | ||
| 29 | + | ||
| 29 | 30 | ||
| 30 | 31 | ||
| 31 | 32 | ||
| @@ -697,6 +698,13 @@ TEST_F(GraphExecutorMultiStreamSystemTest, Case06_TwoStream_WithStaticSubGraph_o | |||
| 697 | .SetRootModelStreamNum(stream_num) | 698 | .SetRootModelStreamNum(stream_num) |
| 698 | .SetRootModelEventNum(event_num) | 699 | .SetRootModelEventNum(event_num) |
| 699 | .BuildGeRootModel(); | 700 | .BuildGeRootModel(); |
| 701 | + ASSERT_NE(ge_root_model, nullptr); | ||
| 702 | + const auto &root_graph = ge_root_model->GetRootGraph(); | ||
| 703 | + ASSERT_NE(root_graph, nullptr); | ||
| 704 | + auto &models = ge_root_model->GetSubgraphInstanceNameToModel(); | ||
| 705 | + const auto root_model = models.find(root_graph->GetName()); | ||
| 706 | + ASSERT_NE(root_model, models.end()); | ||
| 707 | + ASSERT_TRUE(ge::AttrUtils::SetStr(root_model->second, ge::ATTR_MODEL_AUTO_MULTISTREAM_TUNING_MODE, "LoadBalance:8")); | ||
| 700 | 708 | ||
| 701 | bg::ValueHolder::PopGraphFrame(); // 不需要BgTest自带的Frame | 709 | bg::ValueHolder::PopGraphFrame(); // 不需要BgTest自带的Frame |
| 702 | auto exe_graph = ModelConverter().ConvertGeModelToExecuteGraph(ge_root_model); | 710 | auto exe_graph = ModelConverter().ConvertGeModelToExecuteGraph(ge_root_model); |
| @@ -717,6 +725,8 @@ TEST_F(GraphExecutorMultiStreamSystemTest, Case06_TwoStream_WithStaticSubGraph_o | |||
| 717 | auto i3 = FakeValue<uint64_t>(reinterpret_cast<uint64_t>(stream)); | 725 | auto i3 = FakeValue<uint64_t>(reinterpret_cast<uint64_t>(stream)); |
| 718 | RtSession rt_session; | 726 | RtSession rt_session; |
| 719 | EXPECT_EQ(model_executor->Load(), ge::GRAPH_SUCCESS); | 727 | EXPECT_EQ(model_executor->Load(), ge::GRAPH_SUCCESS); |
| 728 | + runtime_stub.GetSlogStub().SetLevelDebug(); | ||
| 729 | + runtime_stub.GetSlogStub().Clear(); | ||
| 720 | 730 | ||
| 721 | auto outputs = FakeTensors({3, 4, 5, 6}, 2); | 731 | auto outputs = FakeTensors({3, 4, 5, 6}, 2); |
| 722 | auto inputs = FakeTensors({3, 4, 5, 6}, 1); | 732 | auto inputs = FakeTensors({3, 4, 5, 6}, 1); |
| @@ -725,6 +735,14 @@ TEST_F(GraphExecutorMultiStreamSystemTest, Case06_TwoStream_WithStaticSubGraph_o | |||
| 725 | outputs.size()), | 735 | outputs.size()), |
| 726 | ge::GRAPH_SUCCESS); | 736 | ge::GRAPH_SUCCESS); |
| 727 | 737 | ||
| 738 | + // 动态根图执行包含静态子图的 DavinciModelExecute;一次根图执行只允许输出根图的 STEP 记录。 | ||
| 739 | + constexpr char kStepTag[] = "[GE_MS_TUNE][STEP]"; | ||
| 740 | + EXPECT_EQ(runtime_stub.GetSlogStub().CountLog(-1, kStepTag), 1); | ||
| 741 | + EXPECT_NE(runtime_stub.GetSlogStub().FindLog(-1, "[GE_MS_TUNE][STEP] api=ModelV2Executor"), -1); | ||
| 742 | + EXPECT_EQ(runtime_stub.GetSlogStub().CountLog(-1, "[GE_MS_TUNE][STEP] api=NnExecute"), 0); | ||
| 743 | + EXPECT_EQ(runtime_stub.GetSlogStub().CountLog(-1, "[GE_MS_TUNE][STEP] api=Run"), 0); | ||
| 744 | + runtime_stub.GetSlogStub().Clear(); | ||
| 745 | + | ||
| 728 | // check stream in launch arg | 746 | // check stream in launch arg |
| 729 | auto all_rt_streams = runtime_stub.GetRtsRuntimeStub().GetAllRtStreams(); | 747 | auto all_rt_streams = runtime_stub.GetRtsRuntimeStub().GetAllRtStreams(); |
| 730 | ASSERT_EQ(all_rt_streams.size(), stream_num); | 748 | ASSERT_EQ(all_rt_streams.size(), stream_num); |
| @@ -721,4 +721,133 @@ TEST_F(MiniDAGStreamPassTest, RunPass_ProfileMultiNodeHitUsesWeightedLoadBalance | |||
| 721 | EXPECT_GT(context.GetCurrMaxStreamId(), 0); | 721 | EXPECT_GT(context.GetCurrMaxStreamId(), 0); |
| 722 | } | 722 | } |
| 723 | 723 | ||
| 724 | +// -------------------- | ||
| 725 | +// 场景 6:图属性注入与策略分支覆盖率补充 | ||
| 726 | +// -------------------- | ||
| 727 | + | ||
| 728 | +/** | ||
| 729 | + * 场景 6-1: 通过根图属性注入多流模式,option 兜底不设置(寻优工具的注入方式) | ||
| 730 | + * 覆盖:stream_utils GetAutoMultistreamParallelMode 图属性命中分支 | ||
| 731 | + */ | ||
| 732 | +TEST_F(MiniDAGStreamPassTest, GraphAttr_LoadBalanceMode) { | ||
| 733 | + auto compute_graph = gert::ShareGraph::BuildTwoAddNodeKnownShapeGraph(); | ||
| 734 | + ASSERT_NE(compute_graph, nullptr); | ||
| 735 | + ASSERT_TRUE(AttrUtils::SetStr(compute_graph, "ge.autoMultistreamParallelMode", "LoadBalance:8")); | ||
| 736 | + | ||
| 737 | + auto graph = GraphUtilsEx::CreateGraphPtrFromComputeGraph(compute_graph); | ||
| 738 | + ASSERT_NE(graph, nullptr); | ||
| 739 | + | ||
| 740 | + ge::StreamPassContext context(0); | ||
| 741 | + auto ret = RunMiniDAGStreamPass(graph, context); | ||
| 742 | + EXPECT_EQ(ret, ge::SUCCESS); | ||
| 743 | +} | ||
| 744 | + | ||
| 745 | +/** | ||
| 746 | + * 场景 6-2: 图属性注入 default,pass 跳过(default 仅允许图属性设置) | ||
| 747 | + * 覆盖:stream_utils ParseAutoMultistreamParallelMode default-from-graph 分支、 | ||
| 748 | + * dag_stream_allocator_pass default 跳过分支 | ||
| 749 | + */ | ||
| 750 | +TEST_F(MiniDAGStreamPassTest, GraphAttr_DefaultModeSkips) { | ||
| 751 | + auto compute_graph = gert::ShareGraph::BuildTwoAddNodeKnownShapeGraph(); | ||
| 752 | + ASSERT_NE(compute_graph, nullptr); | ||
| 753 | + ASSERT_TRUE(AttrUtils::SetStr(compute_graph, "ge.autoMultistreamParallelMode", "default")); | ||
| 754 | + | ||
| 755 | + auto graph = GraphUtilsEx::CreateGraphPtrFromComputeGraph(compute_graph); | ||
| 756 | + ASSERT_NE(graph, nullptr); | ||
| 757 | + | ||
| 758 | + ge::StreamPassContext context(0); | ||
| 759 | + auto ret = RunMiniDAGStreamPass(graph, context); | ||
| 760 | + EXPECT_EQ(ret, ge::SUCCESS); | ||
| 761 | +} | ||
| 762 | + | ||
| 763 | +/** | ||
| 764 | + * 场景 6-3: option 设置 MainStream 策略,走 kMainStream 分支 | ||
| 765 | + * 覆盖:ToMiniDagStrategy kMainStream 分支、ParseAutoMultistreamParallelMode MainStream 分支 | ||
| 766 | + */ | ||
| 767 | +TEST_F(MiniDAGStreamPassTest, Option_MainStreamMode) { | ||
| 768 | + GraphOptionGuard guard; | ||
| 769 | + std::map<std::string, std::string> options; | ||
| 770 | + options["ge.autoMultistreamParallelMode"] = "MainStream:4"; | ||
| 771 | + SetGraphOptionForTest(options); | ||
| 772 | + | ||
| 773 | + auto compute_graph = gert::ShareGraph::BuildTwoAddNodeKnownShapeGraph(); | ||
| 774 | + ASSERT_NE(compute_graph, nullptr); | ||
| 775 | + auto graph = GraphUtilsEx::CreateGraphPtrFromComputeGraph(compute_graph); | ||
| 776 | + ASSERT_NE(graph, nullptr); | ||
| 777 | + | ||
| 778 | + ge::StreamPassContext context(0); | ||
| 779 | + auto ret = RunMiniDAGStreamPass(graph, context); | ||
| 780 | + EXPECT_EQ(ret, ge::SUCCESS); | ||
| 781 | + EXPECT_GT(context.GetCurrMaxStreamId(), 0); | ||
| 782 | +} | ||
| 783 | + | ||
| 784 | +/** | ||
| 785 | + * 场景 6-4: 图属性注入 MainStream 策略 | ||
| 786 | + * 覆盖:图属性读取 + ToMiniDagStrategy kMainStream 的组合路径 | ||
| 787 | + */ | ||
| 788 | +TEST_F(MiniDAGStreamPassTest, GraphAttr_MainStreamMode) { | ||
| 789 | + auto compute_graph = gert::ShareGraph::BuildTwoAddNodeKnownShapeGraph(); | ||
| 790 | + ASSERT_NE(compute_graph, nullptr); | ||
| 791 | + ASSERT_TRUE(AttrUtils::SetStr(compute_graph, "ge.autoMultistreamParallelMode", "MainStream:4")); | ||
| 792 | + | ||
| 793 | + auto graph = GraphUtilsEx::CreateGraphPtrFromComputeGraph(compute_graph); | ||
| 794 | + ASSERT_NE(graph, nullptr); | ||
| 795 | + | ||
| 796 | + ge::StreamPassContext context(0); | ||
| 797 | + auto ret = RunMiniDAGStreamPass(graph, context); | ||
| 798 | + EXPECT_EQ(ret, ge::SUCCESS); | ||
| 799 | +} | ||
| 800 | + | ||
| 801 | +/** | ||
| 802 | + * 场景 6-5: 图属性注入非法流数量,解析失败返回 FAILED | ||
| 803 | + * 覆盖:ParseAutoMultistreamParallelMode 流数量非法报错分支 | ||
| 804 | + */ | ||
| 805 | +TEST_F(MiniDAGStreamPassTest, GraphAttr_InvalidStreamNumFails) { | ||
| 806 | + auto compute_graph = gert::ShareGraph::BuildTwoAddNodeKnownShapeGraph(); | ||
| 807 | + ASSERT_NE(compute_graph, nullptr); | ||
| 808 | + ASSERT_TRUE(AttrUtils::SetStr(compute_graph, "ge.autoMultistreamParallelMode", "MainStream:0")); | ||
| 809 | + | ||
| 810 | + auto graph = GraphUtilsEx::CreateGraphPtrFromComputeGraph(compute_graph); | ||
| 811 | + ASSERT_NE(graph, nullptr); | ||
| 812 | + | ||
| 813 | + ge::StreamPassContext context(0); | ||
| 814 | + auto ret = RunMiniDAGStreamPass(graph, context); | ||
| 815 | + EXPECT_NE(ret, ge::SUCCESS); | ||
| 816 | +} | ||
| 817 | + | ||
| 818 | +/** | ||
| 819 | + * 场景 6-6: default 从 option 设置,解析失败返回 FAILED | ||
| 820 | + * 覆盖:ParseAutoMultistreamParallelMode default-from-option 报错分支 | ||
| 821 | + */ | ||
| 822 | +TEST_F(MiniDAGStreamPassTest, Option_DefaultFromOptionFails) { | ||
| 823 | + GraphOptionGuard guard; | ||
| 824 | + std::map<std::string, std::string> options; | ||
| 825 | + options["ge.autoMultistreamParallelMode"] = "default"; | ||
| 826 | + SetGraphOptionForTest(options); | ||
| 827 | + | ||
| 828 | + auto compute_graph = gert::ShareGraph::BuildTwoAddNodeKnownShapeGraph(); | ||
| 829 | + ASSERT_NE(compute_graph, nullptr); | ||
| 830 | + auto graph = GraphUtilsEx::CreateGraphPtrFromComputeGraph(compute_graph); | ||
| 831 | + ASSERT_NE(graph, nullptr); | ||
| 832 | + | ||
| 833 | + ge::StreamPassContext context(0); | ||
| 834 | + auto ret = RunMiniDAGStreamPass(graph, context); | ||
| 835 | + EXPECT_NE(ret, ge::SUCCESS); | ||
| 836 | +} | ||
| 837 | + | ||
| 838 | +/** | ||
| 839 | + * 场景 6-7: 无 option 且无图属性,直接调用 RunMiniDAGStreamPass 跳过 | ||
| 840 | + * 覆盖:dag_stream_allocator_pass 模式未设置跳过分支 | ||
| 841 | + */ | ||
| 842 | +TEST_F(MiniDAGStreamPassTest, DirectCall_NoModeSkips) { | ||
| 843 | + auto compute_graph = gert::ShareGraph::BuildTwoAddNodeKnownShapeGraph(); | ||
| 844 | + ASSERT_NE(compute_graph, nullptr); | ||
| 845 | + auto graph = GraphUtilsEx::CreateGraphPtrFromComputeGraph(compute_graph); | ||
| 846 | + ASSERT_NE(graph, nullptr); | ||
| 847 | + | ||
| 848 | + ge::StreamPassContext context(0); | ||
| 849 | + auto ret = RunMiniDAGStreamPass(graph, context); | ||
| 850 | + EXPECT_EQ(ret, ge::SUCCESS); | ||
| 851 | +} | ||
| 852 | + | ||
| 724 | } // namespace ge | 853 | } // namespace ge |
| @@ -0,0 +1,256 @@ | |||
| 1 | +/** | ||
| 2 | + * Copyright (c) 2026 Huawei Technologies Co., Ltd. | ||
| 3 | + * This program is free software; you can redistribute it and/or modify it under the terms and conditions of | ||
| 4 | + * CANN Open Software License Agreement Version 2.0 (the "License"). | ||
| 5 | + * Please refer to the License for details. You may not use this file except in compliance with the License. | ||
| 6 | + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, | ||
| 7 | + * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. | ||
| 8 | + * See LICENSE in the root of the software repository for the full text of the License. | ||
| 9 | + */ | ||
| 10 | + | ||
| 11 | + | ||
| 12 | + | ||
| 13 | + | ||
| 14 | + | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | +namespace ge { | ||
| 24 | +namespace { | ||
| 25 | +constexpr const char *kStepTag = "[GE_MS_TUNE][STEP]"; | ||
| 26 | +// 消费侧 examples/multi_stream_autotune/ge_ms_autotune.py 的必填字段,改动须两侧同步 | ||
| 27 | +const std::vector<std::string> kRequiredFields = {"api", "mode", "step", "start_us", "end_us", | ||
| 28 | + "cost_us", "sync_us", "ret", "sync_ret"}; | ||
| 29 | + | ||
| 30 | +/// 按寻优工具的口径解析一行 STEP 日志:标记之后全部为 key=value | ||
| 31 | +std::map<std::string, std::string> ParseStepLine(const std::string &line) { | ||
| 32 | + std::map<std::string, std::string> fields; | ||
| 33 | + const auto position = line.find(kStepTag); | ||
| 34 | + if (position == std::string::npos) { | ||
| 35 | + return fields; | ||
| 36 | + } | ||
| 37 | + std::istringstream stream(line.substr(position + strlen(kStepTag))); | ||
| 38 | + std::string token; | ||
| 39 | + while (stream >> token) { | ||
| 40 | + const auto separator = token.find('='); | ||
| 41 | + if (separator == std::string::npos) { | ||
| 42 | + break; | ||
| 43 | + } | ||
| 44 | + fields.emplace(token.substr(0U, separator), token.substr(separator + 1U)); | ||
| 45 | + } | ||
| 46 | + return fields; | ||
| 47 | +} | ||
| 48 | + | ||
| 49 | +std::vector<std::map<std::string, std::string>> CollectStepRecords(const gert::GertRuntimeStub &runtime_stub) { | ||
| 50 | + std::vector<std::map<std::string, std::string>> records; | ||
| 51 | + for (const auto &log : runtime_stub.GetSlogStub().GetLogs()) { | ||
| 52 | + if (log.content.find(kStepTag) != std::string::npos) { | ||
| 53 | + records.emplace_back(ParseStepLine(log.content)); | ||
| 54 | + } | ||
| 55 | + } | ||
| 56 | + return records; | ||
| 57 | +} | ||
| 58 | + | ||
| 59 | +/** | ||
| 60 | + * 用例描述:StepScope 输出的 STEP 日志满足与寻优工具之间的格式契约 | ||
| 61 | + * 预置条件:调测身标识非空,分别覆盖 V1 静态同步、V1 静态队列异步、RT2.0 三类打点位置 | ||
| 62 | + * 测试步骤:各构造一次 StepScope 并显式停表,按寻优工具的解析口径校验输出 | ||
| 63 | + * 预期结果:每条记录必填字段齐全、标识字段二选一、cost_us 与时间区间自洽 | ||
| 64 | + */ | ||
| 65 | +TEST(MultiStreamTuningStepRecorderSt, StepLogSatisfiesAutotuneParserContract) { | ||
| 66 | + gert::GertRuntimeStub runtime_stub; | ||
| 67 | + runtime_stub.GetSlogStub().SetLevelDebug(); | ||
| 68 | + | ||
| 69 | + const std::string mode = "LoadBalance:4"; | ||
| 70 | + // 借用标识分配器取一个进程内唯一的 model_id,避免与同进程其它用例的 step 计数串扰 | ||
| 71 | + const uint32_t model_id = multistream_tune::AllocateExecutionId(); | ||
| 72 | + { | ||
| 73 | + multistream_tune::StepScope step(multistream_tune::kSiteNnExecute, mode, model_id); | ||
| 74 | + step.Stop(SUCCESS); | ||
| 75 | + } | ||
| 76 | + { | ||
| 77 | + multistream_tune::StepScope step(multistream_tune::kSiteRun, mode, model_id); | ||
| 78 | + step.Stop(SUCCESS); | ||
| 79 | + } | ||
| 80 | + { | ||
| 81 | + const auto execution_id = multistream_tune::AllocateExecutionId(); | ||
| 82 | + multistream_tune::StepScope step(multistream_tune::kSiteModelV2Executor, mode, execution_id); | ||
| 83 | + step.Stop(SUCCESS); | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + const auto records = CollectStepRecords(runtime_stub); | ||
| 87 | + ASSERT_EQ(records.size(), 3U); | ||
| 88 | + for (const auto &record : records) { | ||
| 89 | + for (const auto &name : kRequiredFields) { | ||
| 90 | + EXPECT_NE(record.find(name), record.end()) << "missing field " << name; | ||
| 91 | + } | ||
| 92 | + // 标识字段必须且只能出现一种 | ||
| 93 | + const bool has_graph = (record.count("session_id") != 0U) && (record.count("graph_id") != 0U); | ||
| 94 | + const bool has_model = (record.count("model_id") != 0U); | ||
| 95 | + EXPECT_NE(has_graph, has_model); | ||
| 96 | + EXPECT_EQ(record.at("mode"), mode); | ||
| 97 | + EXPECT_EQ(record.at("ret"), "0"); | ||
| 98 | + EXPECT_EQ(record.at("sync_ret"), "0"); | ||
| 99 | + const auto start_us = std::stoull(record.at("start_us")); | ||
| 100 | + const auto end_us = std::stoull(record.at("end_us")); | ||
| 101 | + ASSERT_GE(end_us, start_us); | ||
| 102 | + EXPECT_EQ(std::stoull(record.at("cost_us")), end_us - start_us); | ||
| 103 | + } | ||
| 104 | + // step 序号按执行对象各自自增:同一 model_id 的两次执行为 0、1 | ||
| 105 | + EXPECT_EQ(records[0].at("api"), "NnExecute"); | ||
| 106 | + EXPECT_EQ(records[0].at("step"), "0"); | ||
| 107 | + EXPECT_EQ(records[1].at("api"), "Run"); | ||
| 108 | + EXPECT_EQ(records[1].at("step"), "1"); | ||
| 109 | + EXPECT_EQ(records[2].at("api"), "ModelV2Executor"); | ||
| 110 | + EXPECT_EQ(records[2].at("step"), "0"); | ||
| 111 | +} | ||
| 112 | + | ||
| 113 | +/** | ||
| 114 | + * 用例描述:未配置调测标识时执行侧完全静默 | ||
| 115 | + * 预置条件:mode 为空 | ||
| 116 | + * 测试步骤:构造 StepScope 并停表 | ||
| 117 | + * 预期结果:不输出任何 STEP 日志 | ||
| 118 | + */ | ||
| 119 | +TEST(MultiStreamTuningStepRecorderSt, NoStepLogWithoutTuningMode) { | ||
| 120 | + gert::GertRuntimeStub runtime_stub; | ||
| 121 | + runtime_stub.GetSlogStub().SetLevelDebug(); | ||
| 122 | + | ||
| 123 | + { | ||
| 124 | + multistream_tune::StepScope step(multistream_tune::kSiteNnExecute, "", 7U); | ||
| 125 | + step.Stop(SUCCESS); | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | + EXPECT_EQ(runtime_stub.GetSlogStub().CountLog(-1, kStepTag), 0); | ||
| 129 | +} | ||
| 130 | + | ||
| 131 | +/** | ||
| 132 | + * 用例描述:失败收尾不等待流,且析构收尾和显式停表均只记录一次 | ||
| 133 | + * 预置条件:调测标识非空,分别覆盖析构失败和显式失败两条路径 | ||
| 134 | + * 测试步骤:第一个 StepScope 直接析构,第二个重复调用 Stop | ||
| 135 | + * 预期结果:两条记录均为失败,重复 Stop 不产生额外记录 | ||
| 136 | + */ | ||
| 137 | +TEST(MultiStreamTuningStepRecorderSt, FailedAndDestructorStopAreRecordedOnce) { | ||
| 138 | + gert::GertRuntimeStub runtime_stub; | ||
| 139 | + runtime_stub.GetSlogStub().SetLevelDebug(); | ||
| 140 | + const auto execution_id = multistream_tune::AllocateExecutionId(); | ||
| 141 | + { | ||
| 142 | + multistream_tune::StepScope step(multistream_tune::kSiteRun, "LoadBalance:4", execution_id, | ||
| 143 | + reinterpret_cast<void *>(static_cast<uintptr_t>(0x1234U))); | ||
| 144 | + } | ||
| 145 | + { | ||
| 146 | + multistream_tune::StepScope step(multistream_tune::kSiteRun, "LoadBalance:4", execution_id, | ||
| 147 | + reinterpret_cast<void *>(static_cast<uintptr_t>(0x1234U))); | ||
| 148 | + step.Stop(FAILED); | ||
| 149 | + step.Stop(SUCCESS); | ||
| 150 | + } | ||
| 151 | + | ||
| 152 | + const auto records = CollectStepRecords(runtime_stub); | ||
| 153 | + ASSERT_EQ(records.size(), 2U); | ||
| 154 | + EXPECT_EQ(records[0].at("step"), "0"); | ||
| 155 | + EXPECT_EQ(records[1].at("step"), "1"); | ||
| 156 | + EXPECT_NE(records[0].at("ret"), "0"); | ||
| 157 | + EXPECT_NE(records[1].at("ret"), "0"); | ||
| 158 | + EXPECT_EQ(records[0].at("sync_us"), "0"); | ||
| 159 | + EXPECT_EQ(records[1].at("sync_us"), "0"); | ||
| 160 | +} | ||
| 161 | + | ||
| 162 | +/** | ||
| 163 | + * 用例描述:同线程嵌套执行只输出最外层打点 | ||
| 164 | + * 预置条件:外层 StepScope 已激活 | ||
| 165 | + * 测试步骤:构造内层 StepScope 并分别停表 | ||
| 166 | + * 预期结果:内层退化为空对象,仅保留外层记录 | ||
| 167 | + */ | ||
| 168 | +TEST(MultiStreamTuningStepRecorderSt, NestedStepScopeIsSuppressedOnSameThread) { | ||
| 169 | + gert::GertRuntimeStub runtime_stub; | ||
| 170 | + runtime_stub.GetSlogStub().SetLevelDebug(); | ||
| 171 | + { | ||
| 172 | + multistream_tune::StepScope outer(multistream_tune::kSiteNnExecute, "LoadBalance:4", | ||
| 173 | + multistream_tune::AllocateExecutionId()); | ||
| 174 | + { | ||
| 175 | + multistream_tune::StepScope inner(multistream_tune::kSiteModelV2Executor, "LoadBalance:4", | ||
| 176 | + multistream_tune::AllocateExecutionId()); | ||
| 177 | + inner.Stop(SUCCESS); | ||
| 178 | + } | ||
| 179 | + outer.Stop(SUCCESS); | ||
| 180 | + } | ||
| 181 | + | ||
| 182 | + EXPECT_EQ(runtime_stub.GetSlogStub().CountLog(-1, kStepTag), 1); | ||
| 183 | + EXPECT_NE(runtime_stub.GetSlogStub().FindLog(-1, "api=NnExecute"), -1); | ||
| 184 | + EXPECT_EQ(runtime_stub.GetSlogStub().FindLog(-1, "api=ModelV2Executor"), -1); | ||
| 185 | +} | ||
| 186 | + | ||
| 187 | +/** | ||
| 188 | + * 用例描述:不同线程的执行打点互不抑制 | ||
| 189 | + * 预置条件:主线程持有活跃 StepScope | ||
| 190 | + * 测试步骤:工作线程构造并停表另一个 StepScope | ||
| 191 | + * 预期结果:主线程和工作线程各输出一条记录 | ||
| 192 | + */ | ||
| 193 | +TEST(MultiStreamTuningStepRecorderSt, StepScopeIsIndependentAcrossThreads) { | ||
| 194 | + gert::GertRuntimeStub runtime_stub; | ||
| 195 | + runtime_stub.GetSlogStub().SetLevelDebug(); | ||
| 196 | + { | ||
| 197 | + multistream_tune::StepScope outer(multistream_tune::kSiteNnExecute, "LoadBalance:4", | ||
| 198 | + multistream_tune::AllocateExecutionId()); | ||
| 199 | + std::thread worker([]() { | ||
| 200 | + multistream_tune::StepScope inner(multistream_tune::kSiteRun, "LoadBalance:4", | ||
| 201 | + multistream_tune::AllocateExecutionId()); | ||
| 202 | + inner.Stop(SUCCESS); | ||
| 203 | + }); | ||
| 204 | + worker.join(); | ||
| 205 | + outer.Stop(SUCCESS); | ||
| 206 | + } | ||
| 207 | + | ||
| 208 | + EXPECT_EQ(runtime_stub.GetSlogStub().CountLog(-1, kStepTag), 2); | ||
| 209 | +} | ||
| 210 | + | ||
| 211 | +/** | ||
| 212 | + * 用例描述:模式串脱敏并限制长度,避免破坏 STEP key=value 格式 | ||
| 213 | + * 预置条件:模式串包含换行和超长内容 | ||
| 214 | + * 测试步骤:构造 StepScope 并停表 | ||
| 215 | + * 预期结果:非法字符替换为下划线,输出模式长度不超过 128 | ||
| 216 | + */ | ||
| 217 | +TEST(MultiStreamTuningStepRecorderSt, StepModeIsSanitizedAndTruncated) { | ||
| 218 | + gert::GertRuntimeStub runtime_stub; | ||
| 219 | + runtime_stub.GetSlogStub().SetLevelDebug(); | ||
| 220 | + const std::string raw_mode = "Load\nBalance:" + std::string(256U, '8'); | ||
| 221 | + { | ||
| 222 | + multistream_tune::StepScope step(multistream_tune::kSiteNnExecute, raw_mode, | ||
| 223 | + multistream_tune::AllocateExecutionId()); | ||
| 224 | + step.Stop(SUCCESS); | ||
| 225 | + } | ||
| 226 | + | ||
| 227 | + const auto records = CollectStepRecords(runtime_stub); | ||
| 228 | + ASSERT_EQ(records.size(), 1U); | ||
| 229 | + const auto mode = records.front().at("mode"); | ||
| 230 | + EXPECT_EQ(mode.size(), 128U); | ||
| 231 | + EXPECT_EQ(mode.substr(0U, 13U), "Load_Balance:"); | ||
| 232 | +} | ||
| 233 | + | ||
| 234 | +/** | ||
| 235 | + * 用例描述:成功停表时带流路径执行同步并记录同步结果 | ||
| 236 | + * 预置条件:构造带非空流句柄的 StepScope | ||
| 237 | + * 测试步骤:显式成功停表 | ||
| 238 | + * 预期结果:输出成功记录,sync_ret 为成功 | ||
| 239 | + */ | ||
| 240 | +TEST(MultiStreamTuningStepRecorderSt, SuccessfulStopWithStreamRecordsSyncResult) { | ||
| 241 | + gert::GertRuntimeStub runtime_stub; | ||
| 242 | + runtime_stub.GetSlogStub().SetLevelDebug(); | ||
| 243 | + { | ||
| 244 | + multistream_tune::StepScope step(multistream_tune::kSiteNnExecute, "LoadBalance:4", | ||
| 245 | + multistream_tune::AllocateExecutionId(), | ||
| 246 | + reinterpret_cast<void *>(static_cast<uintptr_t>(0x1234U))); | ||
| 247 | + step.Stop(SUCCESS); | ||
| 248 | + } | ||
| 249 | + | ||
| 250 | + const auto records = CollectStepRecords(runtime_stub); | ||
| 251 | + ASSERT_EQ(records.size(), 1U); | ||
| 252 | + EXPECT_EQ(records.front().at("ret"), "0"); | ||
| 253 | + EXPECT_EQ(records.front().at("sync_ret"), "0"); | ||
| 254 | +} | ||
| 255 | +} // namespace | ||
| 256 | +} // namespace ge | ||
| @@ -548,6 +548,7 @@ set(MULTI_PARTS_TEST_FILES | |||
| 548 | "session/omg_omg_unittest.cc" | 548 | "session/omg_omg_unittest.cc" |
| 549 | "session/single_op_parser_unittest.cc" | 549 | "session/single_op_parser_unittest.cc" |
| 550 | "session/ge_api_unittest.cc" | 550 | "session/ge_api_unittest.cc" |
| 551 | + "common/multi_stream_tuning_step_recorder_unittest.cc" | ||
| 551 | "session/inner_session_unittest.cc" | 552 | "session/inner_session_unittest.cc" |
| 552 | "session/user_hybrid_graph_manager_unittest.cc" | 553 | "session/user_hybrid_graph_manager_unittest.cc" |
| 553 | "session/session_manager_unittest.cc" | 554 | "session/session_manager_unittest.cc" |
| @@ -778,6 +779,7 @@ target_link_libraries(ut_libge_multiparts_utest | |||
| 778 | custom_op_registry_static | 779 | custom_op_registry_static |
| 779 | graph | 780 | graph |
| 780 | graph_base | 781 | graph_base |
| 782 | + ge_common_base | ||
| 781 | ge_executor_shared | 783 | ge_executor_shared |
| 782 | hybrid_executor | 784 | hybrid_executor |
| 783 | davinci_executor | 785 | davinci_executor |
| @@ -0,0 +1,323 @@ | |||
| 1 | +/** | ||
| 2 | + * Copyright (c) 2026 Huawei Technologies Co., Ltd. | ||
| 3 | + * This program is free software; you can redistribute it and/or modify it under the terms and conditions of | ||
| 4 | + * CANN Open Software License Agreement Version 2.0 (the "License"). | ||
| 5 | + * Please refer to the License for details. You may not use this file except in compliance with the License. | ||
| 6 | + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, | ||
| 7 | + * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. | ||
| 8 | + * See LICENSE in the root of the software repository for the full text of the License. | ||
| 9 | + */ | ||
| 10 | + | ||
| 11 | + | ||
| 12 | + | ||
| 13 | + | ||
| 14 | + | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + | ||
| 24 | + | ||
| 25 | + | ||
| 26 | + | ||
| 27 | + | ||
| 28 | +namespace ge { | ||
| 29 | +namespace { | ||
| 30 | +constexpr const char *kStepTag = "[GE_MS_TUNE][STEP]"; | ||
| 31 | +constexpr const char *kMode = "LoadBalance:8"; | ||
| 32 | + | ||
| 33 | +class RecordingAclRuntimeStub : public AclRuntimeStub { | ||
| 34 | + public: | ||
| 35 | + aclError aclrtSynchronizeStreamWithTimeout(aclrtStream stream, int32_t timeout) override { | ||
| 36 | + ++timed_sync_count; | ||
| 37 | + last_stream = stream; | ||
| 38 | + last_timeout = timeout; | ||
| 39 | + return stream_sync_ret; | ||
| 40 | + } | ||
| 41 | + | ||
| 42 | + // 打点不得使用无超时同步:一旦被调用即视为回归 | ||
| 43 | + aclError aclrtSynchronizeStream(aclrtStream stream) override { | ||
| 44 | + ++untimed_sync_count; | ||
| 45 | + last_stream = stream; | ||
| 46 | + return stream_sync_ret; | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + int32_t timed_sync_count = 0; | ||
| 50 | + int32_t untimed_sync_count = 0; | ||
| 51 | + int32_t last_timeout = 0; | ||
| 52 | + aclrtStream last_stream = nullptr; | ||
| 53 | + aclError stream_sync_ret = ACL_SUCCESS; | ||
| 54 | +}; | ||
| 55 | + | ||
| 56 | +class RecordingSlogStub : public SlogStub { | ||
| 57 | + public: | ||
| 58 | + void Log(int32_t, int32_t, const char *const format, va_list args) override { | ||
| 59 | + char buffer[2048] = {}; | ||
| 60 | + if (vsnprintf_s(buffer, sizeof(buffer), sizeof(buffer) - 1U, format, args) >= 0) { | ||
| 61 | + const std::lock_guard<std::mutex> lock(mutex_); | ||
| 62 | + logs_.emplace_back(buffer); | ||
| 63 | + } | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + std::vector<std::string> StepLogs() const { | ||
| 67 | + const std::lock_guard<std::mutex> lock(mutex_); | ||
| 68 | + std::vector<std::string> steps; | ||
| 69 | + for (const auto &log : logs_) { | ||
| 70 | + if (log.find(kStepTag) != std::string::npos) { | ||
| 71 | + steps.emplace_back(log); | ||
| 72 | + } | ||
| 73 | + } | ||
| 74 | + return steps; | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + bool Empty() const { | ||
| 78 | + const std::lock_guard<std::mutex> lock(mutex_); | ||
| 79 | + return logs_.empty(); | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + private: | ||
| 83 | + mutable std::mutex mutex_; | ||
| 84 | + std::vector<std::string> logs_; | ||
| 85 | +}; | ||
| 86 | + | ||
| 87 | +class MultiStreamTuningStepRecorderTest : public testing::Test { | ||
| 88 | + protected: | ||
| 89 | + void SetUp() override { | ||
| 90 | + runtime_stub_ = std::make_shared<RecordingAclRuntimeStub>(); | ||
| 91 | + slog_stub_ = std::make_shared<RecordingSlogStub>(); | ||
| 92 | + AclRuntimeStub::SetInstance(runtime_stub_); | ||
| 93 | + SlogStub::SetInstance(slog_stub_); | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + void TearDown() override { | ||
| 97 | + SlogStub::SetInstance(nullptr); | ||
| 98 | + AclRuntimeStub::Reset(); | ||
| 99 | + } | ||
| 100 | + | ||
| 101 | + // 执行对象标识逐用例取新值,避免共享的 step 计数器在用例间串扰 | ||
| 102 | + static uint32_t NewExecutionId() { | ||
| 103 | + return multistream_tune::AllocateExecutionId(); | ||
| 104 | + } | ||
| 105 | + | ||
| 106 | + std::shared_ptr<RecordingAclRuntimeStub> runtime_stub_; | ||
| 107 | + std::shared_ptr<RecordingSlogStub> slog_stub_; | ||
| 108 | +}; | ||
| 109 | + | ||
| 110 | +/** | ||
| 111 | + * 用例描述:调测标识为空时打点保持零开销 | ||
| 112 | + * 预置条件:以空 mode 构造 StepScope,并传入非空 stream | ||
| 113 | + * 测试步骤:显式停表后析构 | ||
| 114 | + * 预期结果:不输出日志、不发起流同步 | ||
| 115 | + */ | ||
| 116 | +TEST_F(MultiStreamTuningStepRecorderTest, EmptyModeKeepsZeroOverhead) { | ||
| 117 | + { | ||
| 118 | + multistream_tune::StepScope step(multistream_tune::kSiteNnExecute, "", NewExecutionId(), | ||
| 119 | + reinterpret_cast<void *>(static_cast<uintptr_t>(0x1234U))); | ||
| 120 | + step.Stop(SUCCESS); | ||
| 121 | + } | ||
| 122 | + EXPECT_EQ(runtime_stub_->timed_sync_count, 0); | ||
| 123 | + EXPECT_EQ(runtime_stub_->untimed_sync_count, 0); | ||
| 124 | + EXPECT_TRUE(slog_stub_->Empty()); | ||
| 125 | +} | ||
| 126 | + | ||
| 127 | +/** | ||
| 128 | + * 用例描述:显式停表输出完整字段且同步指定 stream | ||
| 129 | + * 预置条件:构造带 stream 的 StepScope | ||
| 130 | + * 测试步骤:Stop(SUCCESS) 后检查日志字段 | ||
| 131 | + * 预期结果:输出一条含 api/mode/model_id/step 等字段的记录,且同步了传入 stream | ||
| 132 | + */ | ||
| 133 | +TEST_F(MultiStreamTuningStepRecorderTest, StopRecordsAllContractFields) { | ||
| 134 | + auto *const stream = reinterpret_cast<void *>(static_cast<uintptr_t>(0x1234U)); | ||
| 135 | + const auto execution_id = NewExecutionId(); | ||
| 136 | + { | ||
| 137 | + multistream_tune::StepScope step(multistream_tune::kSiteNnExecute, kMode, execution_id, stream); | ||
| 138 | + step.Stop(SUCCESS); | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + const auto steps = slog_stub_->StepLogs(); | ||
| 142 | + ASSERT_EQ(steps.size(), 1U); | ||
| 143 | + const auto &log = steps.front(); | ||
| 144 | + EXPECT_NE(log.find("api=NnExecute "), std::string::npos); | ||
| 145 | + EXPECT_NE(log.find(std::string("mode=") + kMode + " "), std::string::npos); | ||
| 146 | + EXPECT_NE(log.find("model_id=" + std::to_string(execution_id) + " "), std::string::npos); | ||
| 147 | + EXPECT_NE(log.find("step=0 "), std::string::npos); | ||
| 148 | + EXPECT_NE(log.find(" cost_us="), std::string::npos); | ||
| 149 | + EXPECT_NE(log.find(" sync_us="), std::string::npos); | ||
| 150 | + EXPECT_NE(log.find(" ret=0 "), std::string::npos); | ||
| 151 | + EXPECT_NE(log.find(" sync_ret=0"), std::string::npos); | ||
| 152 | + // 在线场景不输出 session_id/graph_id,标识字段严格二选一 | ||
| 153 | + EXPECT_EQ(log.find("session_id="), std::string::npos); | ||
| 154 | + EXPECT_EQ(log.find("graph_id="), std::string::npos); | ||
| 155 | + // 同步须走带超时接口,沿用调用方配置的超时,不得绕过既有超时保护 | ||
| 156 | + EXPECT_EQ(runtime_stub_->timed_sync_count, 1); | ||
| 157 | + EXPECT_EQ(runtime_stub_->untimed_sync_count, 0); | ||
| 158 | + EXPECT_EQ(runtime_stub_->last_timeout, GetContext().StreamSyncTimeout()); | ||
| 159 | + EXPECT_EQ(runtime_stub_->last_stream, static_cast<aclrtStream>(stream)); | ||
| 160 | +} | ||
| 161 | + | ||
| 162 | +/** | ||
| 163 | + * 用例描述:早退析构不得同步 stream | ||
| 164 | + * 预置条件:带非空 stream 构造 StepScope,模拟下发失败/同步超时后的直接返回 | ||
| 165 | + * 测试步骤:不调用 Stop,直接离开作用域 | ||
| 166 | + * 预期结果:不发起任何流同步,仍落一条 ret 非 0 的记录 | ||
| 167 | + */ | ||
| 168 | +TEST_F(MultiStreamTuningStepRecorderTest, DestructorDoesNotSynchronizeStream) { | ||
| 169 | + auto *const stream = reinterpret_cast<void *>(static_cast<uintptr_t>(0x1234U)); | ||
| 170 | + { | ||
| 171 | + multistream_tune::StepScope step(multistream_tune::kSiteNnExecute, kMode, NewExecutionId(), stream); | ||
| 172 | + } | ||
| 173 | + | ||
| 174 | + EXPECT_EQ(runtime_stub_->timed_sync_count, 0); | ||
| 175 | + EXPECT_EQ(runtime_stub_->untimed_sync_count, 0); | ||
| 176 | + const auto steps = slog_stub_->StepLogs(); | ||
| 177 | + ASSERT_EQ(steps.size(), 1U); | ||
| 178 | + EXPECT_EQ(steps.front().find(" ret=0 "), std::string::npos); | ||
| 179 | + EXPECT_NE(steps.front().find(" sync_us=0 "), std::string::npos); | ||
| 180 | +} | ||
| 181 | + | ||
| 182 | +/** | ||
| 183 | + * 用例描述:显式以失败停表同样不同步 stream | ||
| 184 | + * 预置条件:带非空 stream 构造 StepScope,模拟任务下发前的校验/准备失败 | ||
| 185 | + * 测试步骤:以失败返回值 Stop 后离开作用域 | ||
| 186 | + * 预期结果:不发起任何流同步(任务可能未下发,不应等待该流上的历史任务),仅落一条失败记录 | ||
| 187 | + */ | ||
| 188 | +TEST_F(MultiStreamTuningStepRecorderTest, FailedStopDoesNotSynchronizeStream) { | ||
| 189 | + auto *const stream = reinterpret_cast<void *>(static_cast<uintptr_t>(0x1234U)); | ||
| 190 | + { | ||
| 191 | + multistream_tune::StepScope step(multistream_tune::kSiteModelV2Executor, kMode, NewExecutionId(), stream); | ||
| 192 | + step.Stop(FAILED); | ||
| 193 | + } | ||
| 194 | + | ||
| 195 | + EXPECT_EQ(runtime_stub_->timed_sync_count, 0); | ||
| 196 | + EXPECT_EQ(runtime_stub_->untimed_sync_count, 0); | ||
| 197 | + const auto steps = slog_stub_->StepLogs(); | ||
| 198 | + ASSERT_EQ(steps.size(), 1U); | ||
| 199 | + EXPECT_EQ(steps.front().find(" ret=0 "), std::string::npos); | ||
| 200 | + EXPECT_NE(steps.front().find(" sync_us=0 "), std::string::npos); | ||
| 201 | +} | ||
| 202 | + | ||
| 203 | +/** | ||
| 204 | + * 用例描述:未显式停表时由析构按失败收尾,重复停表幂等 | ||
| 205 | + * 预置条件:分别构造仅析构和重复 Stop 的两个 StepScope | ||
| 206 | + * 测试步骤:第一个直接离开作用域,第二个连续 Stop 两次 | ||
| 207 | + * 预期结果:各输出一条记录,早退记录的 ret 非 0 | ||
| 208 | + */ | ||
| 209 | +TEST_F(MultiStreamTuningStepRecorderTest, DestructorFallsBackAndStopIsIdempotent) { | ||
| 210 | + const auto execution_id = NewExecutionId(); | ||
| 211 | + { | ||
| 212 | + multistream_tune::StepScope step(multistream_tune::kSiteRun, kMode, execution_id); | ||
| 213 | + } | ||
| 214 | + { | ||
| 215 | + multistream_tune::StepScope step(multistream_tune::kSiteRun, kMode, execution_id); | ||
| 216 | + step.Stop(SUCCESS); | ||
| 217 | + step.Stop(FAILED); | ||
| 218 | + } | ||
| 219 | + | ||
| 220 | + const auto steps = slog_stub_->StepLogs(); | ||
| 221 | + ASSERT_EQ(steps.size(), 2U); | ||
| 222 | + EXPECT_NE(steps[0].find("step=0 "), std::string::npos); | ||
| 223 | + EXPECT_EQ(steps[0].find(" ret=0 "), std::string::npos); | ||
| 224 | + // step 序号按执行对象自增 | ||
| 225 | + EXPECT_NE(steps[1].find("step=1 "), std::string::npos); | ||
| 226 | + EXPECT_NE(steps[1].find(" ret=0 "), std::string::npos); | ||
| 227 | +} | ||
| 228 | + | ||
| 229 | +/** | ||
| 230 | + * 用例描述:同线程嵌套只统计最外层 | ||
| 231 | + * 预置条件:在外层 StepScope 作用域内再构造内层 StepScope | ||
| 232 | + * 测试步骤:内外层均显式停表 | ||
| 233 | + * 预期结果:仅输出外层一条记录 | ||
| 234 | + */ | ||
| 235 | +TEST_F(MultiStreamTuningStepRecorderTest, NestedScopeOnSameThreadIsSuppressed) { | ||
| 236 | + { | ||
| 237 | + multistream_tune::StepScope outer(multistream_tune::kSiteNnExecute, kMode, NewExecutionId()); | ||
| 238 | + { | ||
| 239 | + multistream_tune::StepScope inner(multistream_tune::kSiteModelV2Executor, kMode, NewExecutionId()); | ||
| 240 | + inner.Stop(SUCCESS); | ||
| 241 | + } | ||
| 242 | + outer.Stop(SUCCESS); | ||
| 243 | + } | ||
| 244 | + | ||
| 245 | + const auto steps = slog_stub_->StepLogs(); | ||
| 246 | + ASSERT_EQ(steps.size(), 1U); | ||
| 247 | + EXPECT_NE(steps.front().find("api=NnExecute "), std::string::npos); | ||
| 248 | +} | ||
| 249 | + | ||
| 250 | +/** | ||
| 251 | + * 用例描述:跨线程不抑制,队列异步路径的 worker 线程(DavinciModel::Run)可独立打点 | ||
| 252 | + * 预置条件:主线程持有活跃 StepScope | ||
| 253 | + * 测试步骤:另起线程在其作用域内构造并停表 | ||
| 254 | + * 预期结果:两条记录均输出 | ||
| 255 | + */ | ||
| 256 | +TEST_F(MultiStreamTuningStepRecorderTest, NestedScopeOnOtherThreadIsNotSuppressed) { | ||
| 257 | + { | ||
| 258 | + multistream_tune::StepScope outer(multistream_tune::kSiteNnExecute, kMode, NewExecutionId()); | ||
| 259 | + std::thread worker([this]() { | ||
| 260 | + multistream_tune::StepScope inner(multistream_tune::kSiteRun, kMode, NewExecutionId()); | ||
| 261 | + inner.Stop(SUCCESS); | ||
| 262 | + }); | ||
| 263 | + worker.join(); | ||
| 264 | + outer.Stop(SUCCESS); | ||
| 265 | + } | ||
| 266 | + | ||
| 267 | + EXPECT_EQ(slog_stub_->StepLogs().size(), 2U); | ||
| 268 | +} | ||
| 269 | + | ||
| 270 | +/** | ||
| 271 | + * 用例描述:模式串中的非法字符被脱敏且长度受限 | ||
| 272 | + * 预置条件:构造含换行与超长片段的模式串 | ||
| 273 | + * 测试步骤:停表后检查 mode 字段 | ||
| 274 | + * 预期结果:换行被替换为下划线,模式串被截断到上限长度 | ||
| 275 | + */ | ||
| 276 | +TEST_F(MultiStreamTuningStepRecorderTest, ModeIsSanitizedAndTruncated) { | ||
| 277 | + const std::string raw_mode = "Load\nBalance:" + std::string(256U, '8'); | ||
| 278 | + { | ||
| 279 | + multistream_tune::StepScope step(multistream_tune::kSiteNnExecute, raw_mode, NewExecutionId()); | ||
| 280 | + step.Stop(SUCCESS); | ||
| 281 | + } | ||
| 282 | + | ||
| 283 | + const auto steps = slog_stub_->StepLogs(); | ||
| 284 | + ASSERT_EQ(steps.size(), 1U); | ||
| 285 | + const std::string expected_mode = "Load_Balance:" + std::string(115U, '8'); | ||
| 286 | + EXPECT_NE(steps.front().find("mode=" + expected_mode + " "), std::string::npos); | ||
| 287 | +} | ||
| 288 | + | ||
| 289 | +/** | ||
| 290 | + * 用例描述:流同步失败时记录 sync_ret,不影响记录输出 | ||
| 291 | + * 预置条件:stream 同步桩返回失败 | ||
| 292 | + * 测试步骤:构造带 stream 的 StepScope 并停表 | ||
| 293 | + * 预期结果:输出记录且 sync_ret 非 0 | ||
| 294 | + */ | ||
| 295 | +TEST_F(MultiStreamTuningStepRecorderTest, StreamSyncFailureIsReported) { | ||
| 296 | + runtime_stub_->stream_sync_ret = ACL_ERROR_RT_INTERNAL_ERROR; | ||
| 297 | + { | ||
| 298 | + multistream_tune::StepScope step(multistream_tune::kSiteNnExecute, kMode, NewExecutionId(), | ||
| 299 | + reinterpret_cast<void *>(static_cast<uintptr_t>(0x1234U))); | ||
| 300 | + step.Stop(SUCCESS); | ||
| 301 | + } | ||
| 302 | + | ||
| 303 | + const auto steps = slog_stub_->StepLogs(); | ||
| 304 | + ASSERT_EQ(steps.size(), 1U); | ||
| 305 | + EXPECT_EQ(steps.front().find(" sync_ret=0"), std::string::npos); | ||
| 306 | + EXPECT_EQ(runtime_stub_->untimed_sync_count, 0); | ||
| 307 | +} | ||
| 308 | + | ||
| 309 | +/** | ||
| 310 | + * 用例描述:无 model_id 的执行器可获得唯一执行对象标识 | ||
| 311 | + * 预置条件:连续申请两个标识 | ||
| 312 | + * 测试步骤:比较两次返回值 | ||
| 313 | + * 预期结果:取值互不相同且位于与 model_id 不重叠的高位区间 | ||
| 314 | + */ | ||
| 315 | +TEST_F(MultiStreamTuningStepRecorderTest, AllocateExecutionIdIsUniqueAndOutOfModelIdRange) { | ||
| 316 | + const auto first = multistream_tune::AllocateExecutionId(); | ||
| 317 | + const auto second = multistream_tune::AllocateExecutionId(); | ||
| 318 | + EXPECT_NE(first, second); | ||
| 319 | + EXPECT_GE(first, 0x7F000000U); | ||
| 320 | + EXPECT_GE(second, 0x7F000000U); | ||
| 321 | +} | ||
| 322 | +} // namespace | ||
| 323 | +} // namespace ge | ||
| @@ -18,6 +18,7 @@ | |||
| 18 | 18 | ||
| 19 | 19 | ||
| 20 | 20 | ||
| 21 | + | ||
| 21 | 22 | ||
| 22 | 23 | ||
| 23 | 24 | ||
| @@ -25,6 +26,8 @@ | |||
| 25 | 26 | ||
| 26 | 27 | ||
| 27 | 28 | ||
| 29 | + | ||
| 30 | + | ||
| 28 | 31 | ||
| 29 | namespace minidag { | 32 | namespace minidag { |
| 30 | 33 | ||
| @@ -118,6 +121,12 @@ ge::ConstGraphPtr BuildGraphWithControlEdge() { | |||
| 118 | 121 | ||
| 119 | return graph; | 122 | return graph; |
| 120 | } | 123 | } |
| 124 | + | ||
| 125 | +void SetAutoMultistreamMode(const ge::ConstGraphPtr &graph, const std::string &mode) { | ||
| 126 | + const auto compute_graph = ge::GraphUtilsEx::GetComputeGraph(*graph); | ||
| 127 | + ASSERT_NE(compute_graph, nullptr); | ||
| 128 | + ASSERT_TRUE(ge::AttrUtils::SetStr(compute_graph, "ge.autoMultistreamParallelMode", mode)); | ||
| 129 | +} | ||
| 121 | } // namespace | 130 | } // namespace |
| 122 | 131 | ||
| 123 | class DagStreamAllocatorPassTest : public testing::Test { | 132 | class DagStreamAllocatorPassTest : public testing::Test { |
| @@ -404,6 +413,17 @@ TEST_F(DagStreamAllocatorPassTest, RunPass_OnlyDataNode) { | |||
| 404 | // 场景 C:ge.autoMultistreamParallelMode 配置测试 | 413 | // 场景 C:ge.autoMultistreamParallelMode 配置测试 |
| 405 | // -------------------- | 414 | // -------------------- |
| 406 | 415 | ||
| 416 | +TEST_F(DagStreamAllocatorPassTest, RunPass_WithoutAutoMultistreamMode_SkipsAllocation) { | ||
| 417 | + GraphOptionGuard graph_option_guard; | ||
| 418 | + SetGraphOptionForTest({}); | ||
| 419 | + auto graph = BuildGraphWithControlEdge(); | ||
| 420 | + ASSERT_NE(graph, nullptr); | ||
| 421 | + | ||
| 422 | + ge::StreamPassContext context(0); | ||
| 423 | + EXPECT_EQ(RunMiniDAGStreamPassForTest(graph, context), ge::SUCCESS); | ||
| 424 | + EXPECT_EQ(context.GetCurrMaxStreamId(), 0); | ||
| 425 | +} | ||
| 426 | + | ||
| 407 | /** | 427 | /** |
| 408 | * 场景 C1: 设置 ge.autoMultistreamParallelMode="LoadBalance:8" - 解析冒号格式 | 428 | * 场景 C1: 设置 ge.autoMultistreamParallelMode="LoadBalance:8" - 解析冒号格式 |
| 409 | */ | 429 | */ |
| @@ -422,6 +442,44 @@ TEST_F(DagStreamAllocatorPassTest, RunPass_WithAutoMultistreamMode_LoadBalance) | |||
| 422 | ge::GetThreadLocalContext().SetGraphOption({}); | 442 | ge::GetThreadLocalContext().SetGraphOption({}); |
| 423 | } | 443 | } |
| 424 | 444 | ||
| 445 | +TEST_F(DagStreamAllocatorPassTest, RunPass_GraphAttributeHasPriorityOverOption) { | ||
| 446 | + GraphOptionGuard graph_option_guard; | ||
| 447 | + auto graph = BuildGraphWithControlEdge(); | ||
| 448 | + ASSERT_NE(graph, nullptr); | ||
| 449 | + SetAutoMultistreamMode(graph, "LoadBalance:8"); | ||
| 450 | + ge::GetThreadLocalContext().SetGraphOption({{"ge.autoMultistreamParallelMode", "MainStream:65"}}); | ||
| 451 | + | ||
| 452 | + ge::StreamPassContext graph_context(0); | ||
| 453 | + EXPECT_EQ(RunMiniDAGStreamPass(graph, graph_context), ge::SUCCESS); | ||
| 454 | + | ||
| 455 | + SetAutoMultistreamMode(graph, "LoadBalance:65"); | ||
| 456 | + ge::GetThreadLocalContext().SetGraphOption({{"ge.autoMultistreamParallelMode", "MainStream:4"}}); | ||
| 457 | + ge::StreamPassContext invalid_graph_context(0); | ||
| 458 | + EXPECT_EQ(RunMiniDAGStreamPass(graph, invalid_graph_context), ge::FAILED); | ||
| 459 | + | ||
| 460 | + SetAutoMultistreamMode(graph, "cv"); | ||
| 461 | + ge::GetThreadLocalContext().SetGraphOption({{"ge.autoMultistreamParallelMode", "LoadBalance:8"}}); | ||
| 462 | + const auto compute_graph = ge::GraphUtilsEx::GetComputeGraph(*graph); | ||
| 463 | + ASSERT_NE(compute_graph, nullptr); | ||
| 464 | + EXPECT_TRUE(ge::StreamUtils::EnableCvParallel(compute_graph)); | ||
| 465 | + ge::StreamPassContext cv_context(0); | ||
| 466 | + EXPECT_EQ(RunMiniDAGStreamPass(graph, cv_context), ge::SUCCESS); | ||
| 467 | + EXPECT_EQ(cv_context.GetCurrMaxStreamId(), 0); | ||
| 468 | +} | ||
| 469 | + | ||
| 470 | +TEST_F(DagStreamAllocatorPassTest, RunPass_DefaultSkipsMiniDagAllocation) { | ||
| 471 | + GraphOptionGuard graph_option_guard; | ||
| 472 | + ge::GetThreadLocalContext().SetGraphOption({{"ge.autoMultistreamParallelMode", "LoadBalance:8"}}); | ||
| 473 | + | ||
| 474 | + auto graph = BuildGraphWithControlEdge(); | ||
| 475 | + ASSERT_NE(graph, nullptr); | ||
| 476 | + SetAutoMultistreamMode(graph, "default"); | ||
| 477 | + | ||
| 478 | + ge::StreamPassContext context(0); | ||
| 479 | + EXPECT_EQ(RunMiniDAGStreamPass(graph, context), ge::SUCCESS); | ||
| 480 | + EXPECT_EQ(context.GetCurrMaxStreamId(), 0); | ||
| 481 | +} | ||
| 482 | + | ||
| 425 | /** | 483 | /** |
| 426 | * 场景 C1.1: profiling 文件命中节点时,stream pass 成功走通入口路径 | 484 | * 场景 C1.1: profiling 文件命中节点时,stream pass 成功走通入口路径 |
| 427 | */ | 485 | */ |
| @@ -638,6 +638,25 @@ TEST_F(UtestLogicalStreamAllocator, test_single_stream_conflicts_with_auto_multi | |||
| 638 | -1); | 638 | -1); |
| 639 | } | 639 | } |
| 640 | 640 | ||
| 641 | +TEST_F(UtestLogicalStreamAllocator, test_single_stream_conflicts_with_auto_multi_stream_graph_attr) { | ||
| 642 | + gert::GertRuntimeStub runtime_stub; | ||
| 643 | + SubGraphInfoPtr subgraph = CreateSubgraph("engine1"); | ||
| 644 | + ComputeGraphPtr whole_graph = std::make_shared<ComputeGraph>("whole_graph"); | ||
| 645 | + ASSERT_TRUE(AttrUtils::SetStr(whole_graph, OPTION_AUTO_MULTISTREAM_PARALLEL_MODE, "MainStream:8")); | ||
| 646 | + vector<EngineConfPtr> confs; | ||
| 647 | + std::map<std::string, int> max_parallel_num; | ||
| 648 | + runtime_stub.GetSlogStub().Clear(); | ||
| 649 | + | ||
| 650 | + const Status status = AssignLogicalStreams({subgraph}, confs, max_parallel_num, whole_graph, true); | ||
| 651 | + | ||
| 652 | + EXPECT_EQ(status, ge::PARAM_INVALID); | ||
| 653 | + EXPECT_NE(runtime_stub.GetSlogStub().FindLog( | ||
| 654 | + DLOG_ERROR, | ||
| 655 | + "Cannot configure both parameters ge.autoMultistreamParallelMode and ge.enableSingleStream " | ||
| 656 | + "simultaneously."), | ||
| 657 | + -1); | ||
| 658 | +} | ||
| 659 | + | ||
| 641 | TEST_F(UtestLogicalStreamAllocator, test_single_stream_and_auto_multi_stream_conflict_precedes_stream_label) { | 660 | TEST_F(UtestLogicalStreamAllocator, test_single_stream_and_auto_multi_stream_conflict_precedes_stream_label) { |
| 642 | gert::GertRuntimeStub runtime_stub; | 661 | gert::GertRuntimeStub runtime_stub; |
| 643 | SetGraphOptionsForTest({{OPTION_AUTO_MULTISTREAM_PARALLEL_MODE, "LoadBalance:8"}}); | 662 | SetGraphOptionsForTest({{OPTION_AUTO_MULTISTREAM_PARALLEL_MODE, "LoadBalance:8"}}); |
| @@ -16,6 +16,8 @@ | |||
| 16 | 16 | ||
| 17 | 17 | ||
| 18 | 18 | ||
| 19 | + | ||
| 20 | + | ||
| 19 | 21 | ||
| 20 | 22 | ||
| 21 | 23 | ||
| @@ -510,6 +512,37 @@ void SetTbeKernelAttrs(const OpDescPtr &op_desc, const std::string &kernel_name, | |||
| 510 | } | 512 | } |
| 511 | } // namespace | 513 | } // namespace |
| 512 | 514 | ||
| 515 | +TEST_F(UtestModelBuilderTest, AutoMultistreamTuningModeIsCopiedFromGraphToGeModel) { | ||
| 516 | + const auto graph = std::make_shared<ComputeGraph>("auto_multistream_tuning_graph"); | ||
| 517 | + ASSERT_TRUE(AttrUtils::SetStr(graph, ATTR_MODEL_AUTO_MULTISTREAM_TUNING_MODE, "LoadBalance:8")); | ||
| 518 | + Graph2SubGraphInfoList subgraphs; | ||
| 519 | + std::map<std::string, int> stream_max_parallel_num; | ||
| 520 | + ModelBuilder builder(0U, graph, subgraphs, stream_max_parallel_num, false); | ||
| 521 | + Model model; | ||
| 522 | + model.SetGraph(graph); | ||
| 523 | + | ||
| 524 | + ASSERT_EQ(builder.BuildModelDefForStream(model), SUCCESS); | ||
| 525 | + std::string mode; | ||
| 526 | + EXPECT_TRUE(AttrUtils::GetStr(&model, ATTR_MODEL_AUTO_MULTISTREAM_TUNING_MODE, mode)); | ||
| 527 | + EXPECT_EQ(mode, "LoadBalance:8"); | ||
| 528 | + | ||
| 529 | + SetDummyModelTaskDef(model); | ||
| 530 | + const auto ge_model = MakeShared<GeModel>(); | ||
| 531 | + ASSERT_NE(ge_model, nullptr); | ||
| 532 | + ASSERT_EQ(builder.SaveDataToModel(model, *ge_model), SUCCESS); | ||
| 533 | + mode.clear(); | ||
| 534 | + EXPECT_TRUE(multistream_tune::GetTuningMode(ge_model, mode)); | ||
| 535 | + EXPECT_EQ(mode, "LoadBalance:8"); | ||
| 536 | + | ||
| 537 | + const auto root_model = MakeShared<GeRootModel>(); | ||
| 538 | + ASSERT_NE(root_model, nullptr); | ||
| 539 | + root_model->SetRootGraph(graph); | ||
| 540 | + root_model->SetSubgraphInstanceNameToModel(graph->GetName(), ge_model); | ||
| 541 | + mode.clear(); | ||
| 542 | + EXPECT_TRUE(multistream_tune::GetTuningMode(root_model, mode)); | ||
| 543 | + EXPECT_EQ(mode, "LoadBalance:8"); | ||
| 544 | +} | ||
| 545 | + | ||
| 513 | TEST_F(UtestModelBuilderTest, SaveDataToModelUsesLastBinForSameKernelNameWithDifferentBins) { | 546 | TEST_F(UtestModelBuilderTest, SaveDataToModelUsesLastBinForSameKernelNameWithDifferentBins) { |
| 514 | auto graph = std::make_shared<ComputeGraph>("duplicate_tbe_kernel_graph"); | 547 | auto graph = std::make_shared<ComputeGraph>("duplicate_tbe_kernel_graph"); |
| 515 | auto first_op = std::make_shared<OpDesc>("first_op", RELU); | 548 | auto first_op = std::make_shared<OpDesc>("first_op", RELU); |
| @@ -12,6 +12,8 @@ | |||
| 12 | 12 | ||
| 13 | 13 | ||
| 14 | 14 | ||
| 15 | + | ||
| 16 | + | ||
| 15 | 17 | ||
| 16 | 18 | ||
| 17 | 19 | ||
| @@ -91,4 +93,72 @@ TEST_F(UtestStreamUtils, EnableDynamicShapeMultiStream_WithOptionModesAndNoEnv_R | |||
| 91 | (void)unsetenv(env_name); | 93 | (void)unsetenv(env_name); |
| 92 | } | 94 | } |
| 93 | } | 95 | } |
| 96 | + | ||
| 97 | +TEST_F(UtestStreamUtils, GetAutoMultistreamParallelMode_ReadsOptionMode) { | ||
| 98 | + const auto option_bak = GetThreadLocalContext().GetAllGraphOptions(); | ||
| 99 | + GetThreadLocalContext().SetGraphOption({{"ge.autoMultistreamParallelMode", "LoadBalance:8"}}); | ||
| 100 | + std::string mode; | ||
| 101 | + EXPECT_EQ(StreamUtils::GetAutoMultistreamParallelMode(mode), GRAPH_SUCCESS); | ||
| 102 | + EXPECT_EQ(mode, "LoadBalance:8"); | ||
| 103 | + GetThreadLocalContext().SetGraphOption(option_bak); | ||
| 104 | +} | ||
| 105 | + | ||
| 106 | +TEST_F(UtestStreamUtils, GetAutoMultistreamParallelMode_GraphAttributeHasPriorityOverOption) { | ||
| 107 | + const auto option_bak = GetThreadLocalContext().GetAllGraphOptions(); | ||
| 108 | + GetThreadLocalContext().SetGraphOption({{"ge.autoMultistreamParallelMode", "MainStream:4"}}); | ||
| 109 | + const auto graph = std::make_shared<ComputeGraph>("graph_attr_priority"); | ||
| 110 | + ASSERT_TRUE(AttrUtils::SetStr(graph, "ge.autoMultistreamParallelMode", "LoadBalance:8")); | ||
| 111 | + | ||
| 112 | + std::string mode; | ||
| 113 | + bool from_graph = false; | ||
| 114 | + EXPECT_EQ(StreamUtils::GetAutoMultistreamParallelMode(graph, mode, from_graph), GRAPH_SUCCESS); | ||
| 115 | + EXPECT_EQ(mode, "LoadBalance:8"); | ||
| 116 | + EXPECT_TRUE(from_graph); | ||
| 117 | + | ||
| 118 | + GetThreadLocalContext().SetGraphOption(option_bak); | ||
| 119 | +} | ||
| 120 | + | ||
| 121 | +TEST_F(UtestStreamUtils, ParseAutoMultistreamParallelMode_ValidModes) { | ||
| 122 | + const std::vector<std::tuple<std::string, AutoMultistreamMode, int32_t>> cases = { | ||
| 123 | + {"", AutoMultistreamMode::kUnset, 0}, | ||
| 124 | + {"cv", AutoMultistreamMode::kCv, 0}, | ||
| 125 | + {"LoadBalance:1", AutoMultistreamMode::kLoadBalance, 1}, | ||
| 126 | + {"MainStream:64", AutoMultistreamMode::kMainStream, 64}, | ||
| 127 | + {"WeightedLoadBalance:8", AutoMultistreamMode::kWeightedLoadBalance, 8}, | ||
| 128 | + }; | ||
| 129 | + for (const auto &test_case : cases) { | ||
| 130 | + AutoMultistreamConfig config; | ||
| 131 | + EXPECT_EQ(StreamUtils::ParseAutoMultistreamParallelMode(std::get<0>(test_case), config), GRAPH_SUCCESS) | ||
| 132 | + << std::get<0>(test_case); | ||
| 133 | + EXPECT_EQ(config.mode, std::get<1>(test_case)) << std::get<0>(test_case); | ||
| 134 | + EXPECT_EQ(config.max_stream_num, std::get<2>(test_case)) << std::get<0>(test_case); | ||
| 135 | + } | ||
| 136 | +} | ||
| 137 | + | ||
| 138 | +TEST_F(UtestStreamUtils, ParseAutoMultistreamParallelMode_DefaultOnlyAllowedForGraphAttribute) { | ||
| 139 | + AutoMultistreamConfig config; | ||
| 140 | + EXPECT_NE(StreamUtils::ParseAutoMultistreamParallelMode("default", config), GRAPH_SUCCESS); | ||
| 141 | + // The third argument marks the value as coming from the graph attribute set by a custom pass. | ||
| 142 | + EXPECT_EQ(StreamUtils::ParseAutoMultistreamParallelMode("default", config, true), GRAPH_SUCCESS); | ||
| 143 | + EXPECT_EQ(config.mode, AutoMultistreamMode::kDefault); | ||
| 144 | + EXPECT_EQ(config.max_stream_num, 0); | ||
| 145 | +} | ||
| 146 | + | ||
| 147 | +TEST_F(UtestStreamUtils, ParseAutoMultistreamParallelMode_InvalidModes) { | ||
| 148 | + const std::vector<std::string> cases = { | ||
| 149 | + "LoadBalance", "MainStream", | ||
| 150 | + "WeightedLoadBalance", "default", | ||
| 151 | + "default:2", "cv:2", | ||
| 152 | + "Unknown:2", "LoadBalance:0", | ||
| 153 | + "LoadBalance:65", "LoadBalance:-1", | ||
| 154 | + "LoadBalance:+1", "LoadBalance:1.0", | ||
| 155 | + "LoadBalance:", ":2", | ||
| 156 | + "LoadBalance:2:3", " LoadBalance:2", | ||
| 157 | + "LoadBalance:2 ", "LoadBalance:99999999999999999999", | ||
| 158 | + }; | ||
| 159 | + for (const auto &mode : cases) { | ||
| 160 | + AutoMultistreamConfig config; | ||
| 161 | + EXPECT_NE(StreamUtils::ParseAutoMultistreamParallelMode(mode, config), GRAPH_SUCCESS) << mode; | ||
| 162 | + } | ||
| 163 | +} | ||
| 94 | } // namespace ge | 164 | } // namespace ge |
| @@ -18,6 +18,8 @@ | |||
| 18 | 18 | ||
| 19 | 19 | ||
| 20 | 20 | ||
| 21 | + | ||
| 22 | + | ||
| 21 | 23 | ||
| 22 | 24 | ||
| 23 | 25 | ||
| @@ -171,6 +173,17 @@ TEST(DavinciModelCustomOpRegistry, SetAndGetCustomOpRegistry) { | |||
| 171 | EXPECT_EQ(model.GetCustomOpRegistry().get(), registry.get()); | 173 | EXPECT_EQ(model.GetCustomOpRegistry().get(), registry.get()); |
| 172 | } | 174 | } |
| 173 | 175 | ||
| 176 | +TEST(DavinciModelAutoMultistreamTuning, AssignCachesTuningMode) { | ||
| 177 | + DavinciModel model(0U, nullptr); | ||
| 178 | + const auto ge_model = MakeShared<GeModel>(); | ||
| 179 | + ASSERT_NE(ge_model, nullptr); | ||
| 180 | + ASSERT_TRUE(AttrUtils::SetStr(ge_model, ATTR_MODEL_AUTO_MULTISTREAM_TUNING_MODE, "LoadBalance:8")); | ||
| 181 | + | ||
| 182 | + model.Assign(ge_model); | ||
| 183 | + | ||
| 184 | + EXPECT_EQ(model.GetAutoMultistreamTuningMode(), "LoadBalance:8"); | ||
| 185 | +} | ||
| 186 | + | ||
| 174 | void TestNnExecuteWithGertTensor() { | 187 | void TestNnExecuteWithGertTensor() { |
| 175 | DavinciModel model(0, nullptr); | 188 | DavinciModel model(0, nullptr); |
| 176 | ComputeGraphPtr graph = MakeShared<ComputeGraph>("default"); | 189 | ComputeGraphPtr graph = MakeShared<ComputeGraph>("default"); |
| @@ -22,6 +22,12 @@ class ModelV2ExecutorTestHelper { | |||
| 22 | static void *GetExecutionData(ModelV2Executor *executor, SubExeGraphType graph_type) { | 22 | static void *GetExecutionData(ModelV2Executor *executor, SubExeGraphType graph_type) { |
| 23 | return executor->graphs_[graph_type].execution_data_; | 23 | return executor->graphs_[graph_type].execution_data_; |
| 24 | } | 24 | } |
| 25 | + static const std::string &GetAutoMultistreamTuningMode(const ModelV2Executor *executor) { | ||
| 26 | + return executor->auto_multistream_tuning_mode_; | ||
| 27 | + } | ||
| 28 | + static uint32_t GetAutoMultistreamTuningId(const ModelV2Executor *executor) { | ||
| 29 | + return executor->auto_multistream_tuning_id_; | ||
| 30 | + } | ||
| 25 | static Node *GetNodeByKernelName(void *execution_data, const char *kernel_name) { | 31 | static Node *GetNodeByKernelName(void *execution_data, const char *kernel_name) { |
| 26 | auto edata = reinterpret_cast<ExecutionData *>(execution_data); | 32 | auto edata = reinterpret_cast<ExecutionData *>(execution_data); |
| 27 | for (size_t i = 0; i < edata->base_ed.node_num; ++i) { | 33 | for (size_t i = 0; i < edata->base_ed.node_num; ++i) { |
| @@ -21,6 +21,7 @@ | |||
| 21 | 21 | ||
| 22 | 22 | ||
| 23 | 23 | ||
| 24 | + | ||
| 24 | 25 | ||
| 25 | 26 | ||
| 26 | 27 | ||
| @@ -100,6 +101,31 @@ TEST_F(ModelV2ExecutorBuilderUT, BuildFromSingleNodeGraph) { | |||
| 100 | ASSERT_EQ(model_executor->UnLoad(), ge::GRAPH_SUCCESS); | 101 | ASSERT_EQ(model_executor->UnLoad(), ge::GRAPH_SUCCESS); |
| 101 | } | 102 | } |
| 102 | 103 | ||
| 104 | +TEST_F(ModelV2ExecutorBuilderUT, BuildCachesAutoMultistreamTuningMode) { | ||
| 105 | + auto compute_graph = ShareGraph::BuildSingleNodeGraph(); | ||
| 106 | + ASSERT_EQ(compute_graph->TopologicalSorting(), ge::GRAPH_SUCCESS); | ||
| 107 | + auto root_model = GeModelBuilder(compute_graph).BuildGeRootModel(); | ||
| 108 | + ASSERT_NE(root_model, nullptr); | ||
| 109 | + const auto &models = root_model->GetSubgraphInstanceNameToModel(); | ||
| 110 | + ASSERT_FALSE(models.empty()); | ||
| 111 | + ASSERT_TRUE( | ||
| 112 | + ge::AttrUtils::SetStr(models.begin()->second, ge::ATTR_MODEL_AUTO_MULTISTREAM_TUNING_MODE, "LoadBalance:8")); | ||
| 113 | + auto global_data = GlobalDataFaker(root_model).FakeWithHandleAiCore("Add", false).Build(); | ||
| 114 | + ModelDescHolder model_desc_holder = ModelDescHolderFaker().Build(); | ||
| 115 | + model_desc_holder.SetSpaceRegistry(SpaceRegistryFaker().Build()); | ||
| 116 | + auto exe_graph = GraphConverter() | ||
| 117 | + .SetModelDescHolder(&model_desc_holder) | ||
| 118 | + .ConvertComputeGraphToExecuteGraph(compute_graph, global_data); | ||
| 119 | + ASSERT_NE(exe_graph, nullptr); | ||
| 120 | + | ||
| 121 | + auto model_executor = ModelV2Executor::Create(exe_graph, root_model); | ||
| 122 | + | ||
| 123 | + ASSERT_NE(model_executor, nullptr); | ||
| 124 | + EXPECT_EQ(ModelV2ExecutorTestHelper::GetAutoMultistreamTuningMode(model_executor.get()), "LoadBalance:8"); | ||
| 125 | + // 打点标识非空时才分配执行对象标识 | ||
| 126 | + EXPECT_NE(ModelV2ExecutorTestHelper::GetAutoMultistreamTuningId(model_executor.get()), 0U); | ||
| 127 | +} | ||
| 128 | + | ||
| 103 | TEST_F(ModelV2ExecutorBuilderUT, RefsHasTheSameAddr) { | 129 | TEST_F(ModelV2ExecutorBuilderUT, RefsHasTheSameAddr) { |
| 104 | auto compute_graph = ShareGraph::BuildSingleNodeGraph(); | 130 | auto compute_graph = ShareGraph::BuildSingleNodeGraph(); |
| 105 | compute_graph->TopologicalSorting(); | 131 | compute_graph->TopologicalSorting(); |
| @@ -10,6 +10,7 @@ | |||
| 10 | 10 | ||
| 11 | 11 | ||
| 12 | 12 | ||
| 13 | + | ||
| 13 | 14 | ||
| 14 | 15 | ||
| 15 | 16 | ||
| @@ -18,11 +19,19 @@ | |||
| 18 | 19 | ||
| 19 | 20 | ||
| 20 | 21 | ||
| 22 | + | ||
| 21 | 23 | ||
| 22 | namespace ge { | 24 | namespace ge { |
| 23 | namespace { | 25 | namespace { |
| 24 | const char *const kEnvName = "ASCEND_OPP_PATH"; | 26 | const char *const kEnvName = "ASCEND_OPP_PATH"; |
| 27 | + | ||
| 28 | +void SetAutoMultistreamMode(const GraphPtr &graph, const char *const mode) { | ||
| 29 | + ASSERT_EQ(graph->SetValid(), GRAPH_SUCCESS); | ||
| 30 | + AttrValue attr_value; | ||
| 31 | + ASSERT_EQ(attr_value.SetAttrValue(AscendString(mode)), GRAPH_SUCCESS); | ||
| 32 | + ASSERT_EQ(graph->SetAttr(AscendString("ge.autoMultistreamParallelMode"), attr_value), GRAPH_SUCCESS); | ||
| 25 | } | 33 | } |
| 34 | +} // namespace | ||
| 26 | class UtestRegisterPass : public testing::Test { | 35 | class UtestRegisterPass : public testing::Test { |
| 27 | protected: | 36 | protected: |
| 28 | void SetUp() {} | 37 | void SetUp() {} |
| @@ -319,6 +328,67 @@ TEST_F(UtestRegisterPass, ConstGraph_AfterBuiltinFusionCustomPass_AndRun_Failed_ | |||
| 319 | EXPECT_EQ(pass_reg_data.GetStage(), CustomPassStage::kAfterBuiltinFusionPass); | 328 | EXPECT_EQ(pass_reg_data.GetStage(), CustomPassStage::kAfterBuiltinFusionPass); |
| 320 | } | 329 | } |
| 321 | 330 | ||
| 331 | +TEST_F(UtestRegisterPass, MiniDAGStreamPass_GraphAttributeOverridesOption) { | ||
| 332 | + const auto option_bak = GetThreadLocalContext().GetAllGraphOptions(); | ||
| 333 | + GetThreadLocalContext().SetGraphOption({{"ge.autoMultistreamParallelMode", "cv"}}); | ||
| 334 | + | ||
| 335 | + bool pass_called = false; | ||
| 336 | + CustomAllocateStreamPassFunc alloc_fn = [&pass_called](const ConstGraphPtr &, StreamPassContext &) -> Status { | ||
| 337 | + pass_called = true; | ||
| 338 | + return SUCCESS; | ||
| 339 | + }; | ||
| 340 | + PassRegistrationData pass_data("MiniDAGStreamPass"); | ||
| 341 | + pass_data.CustomAllocateStreamPassFn(alloc_fn); | ||
| 342 | + CustomPassHelper::Instance().Unload(); | ||
| 343 | + CustomPassHelper::Instance().Insert(pass_data); | ||
| 344 | + | ||
| 345 | + auto graph = std::make_shared<Graph>("test_cv"); | ||
| 346 | + StreamPassContext stream_ctx(0); | ||
| 347 | + EXPECT_EQ(CustomPassHelper::Instance().Run(graph, stream_ctx, CustomPassStage::kAfterAssignLogicStream), SUCCESS); | ||
| 348 | + EXPECT_FALSE(pass_called); | ||
| 349 | + | ||
| 350 | + SetAutoMultistreamMode(graph, "LoadBalance:8"); | ||
| 351 | + EXPECT_EQ(CustomPassHelper::Instance().Run(graph, stream_ctx, CustomPassStage::kAfterAssignLogicStream), SUCCESS); | ||
| 352 | + EXPECT_TRUE(pass_called); | ||
| 353 | + | ||
| 354 | + pass_called = false; | ||
| 355 | + GetThreadLocalContext().SetGraphOption({{"ge.autoMultistreamParallelMode", "LoadBalance:8"}}); | ||
| 356 | + SetAutoMultistreamMode(graph, "default"); | ||
| 357 | + EXPECT_EQ(CustomPassHelper::Instance().Run(graph, stream_ctx, CustomPassStage::kAfterAssignLogicStream), SUCCESS); | ||
| 358 | + EXPECT_FALSE(pass_called); | ||
| 359 | + | ||
| 360 | + GetThreadLocalContext().SetGraphOption(option_bak); | ||
| 361 | +} | ||
| 362 | + | ||
| 363 | +TEST_F(UtestRegisterPass, MiniDAGStreamPass_WithoutModeSkipsPass) { | ||
| 364 | + const auto option_bak = GetThreadLocalContext().GetAllGraphOptions(); | ||
| 365 | + GetThreadLocalContext().SetGraphOption({}); | ||
| 366 | + | ||
| 367 | + bool pass_called = false; | ||
| 368 | + CustomAllocateStreamPassFunc alloc_fn = [&pass_called](const ConstGraphPtr &, StreamPassContext &) -> Status { | ||
| 369 | + pass_called = true; | ||
| 370 | + return SUCCESS; | ||
| 371 | + }; | ||
| 372 | + PassRegistrationData pass_data("MiniDAGStreamPass"); | ||
| 373 | + pass_data.CustomAllocateStreamPassFn(alloc_fn); | ||
| 374 | + CustomPassHelper::Instance().Unload(); | ||
| 375 | + CustomPassHelper::Instance().Insert(pass_data); | ||
| 376 | + | ||
| 377 | + auto graph = std::make_shared<Graph>("test_without_mode"); | ||
| 378 | + StreamPassContext stream_ctx(0); | ||
| 379 | + EXPECT_EQ(CustomPassHelper::Instance().Run(graph, stream_ctx, CustomPassStage::kAfterAssignLogicStream), SUCCESS); | ||
| 380 | + EXPECT_FALSE(pass_called); | ||
| 381 | + | ||
| 382 | + ASSERT_EQ(graph->SetValid(), GRAPH_SUCCESS); | ||
| 383 | + AttrValue invalid_mode; | ||
| 384 | + ASSERT_EQ(invalid_mode.SetAttrValue(static_cast<int64_t>(8)), GRAPH_SUCCESS); | ||
| 385 | + ASSERT_EQ(graph->SetAttr(AscendString("ge.autoMultistreamParallelMode"), invalid_mode), GRAPH_SUCCESS); | ||
| 386 | + EXPECT_EQ(CustomPassHelper::Instance().Run(graph, stream_ctx, CustomPassStage::kAfterAssignLogicStream), SUCCESS); | ||
| 387 | + EXPECT_FALSE(pass_called); | ||
| 388 | + | ||
| 389 | + GetThreadLocalContext().SetGraphOption(option_bak); | ||
| 390 | +} | ||
| 391 | + | ||
| 322 | TEST_F(UtestRegisterPass, CustomPassContext_GetOptionValue) { | 392 | TEST_F(UtestRegisterPass, CustomPassContext_GetOptionValue) { |
| 323 | std::map<std::string, std::string> options_map = {{ge::OPTION_GRAPH_RUN_MODE, "train"}}; | 393 | std::map<std::string, std::string> options_map = {{ge::OPTION_GRAPH_RUN_MODE, "train"}}; |
| 324 | auto option_bak = GetThreadLocalContext().GetAllGraphOptions(); | 394 | auto option_bak = GetThreadLocalContext().GetAllGraphOptions(); |


此条代码评论区间+282至+299
当 ge.enableSingleStream=true,而前置自定义 Pass 仅写入图属性 ge.autoMultistreamParallelMode=LoadBalance:N/MainStream:N 时,编译不会报预期的 E10056,但自动多流也不会执行。寻优流程可能把实际的单流性能错误记录为某个多流候选结果。