已合并
feat: 支持自动多流配置与自动寻优工作流 #4301
KenChow创建于 23 天前
feat: 支持自动多流配置与自动寻优工作流 #4301
已合并
KenChow创建于 23 天前
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#include "graph/build/stream/dag_stream_allocator_pass.h"13#include "graph/build/stream/dag_stream_allocator_pass.h"
14#include "graph/build/stream/dag_adapter.h"14#include "graph/build/stream/dag_adapter.h"
15+#include "graph/build/stream/stream_utils.h"
15#include "graph/build/dag/dag_stream_allocator.h"16#include "graph/build/dag/dag_stream_allocator.h"
16#include "register/register_custom_pass.h"17#include "register/register_custom_pass.h"
17-#include "graph/ge_context.h"
18#include "framework/common/debug/ge_log.h"18#include "framework/common/debug/ge_log.h"
19#include "graph/utils/node_utils.h"19#include "graph/utils/node_utils.h"
20#include "graph/utils/node_adapter.h"20#include "graph/utils/node_adapter.h"
@@ -23,8 +23,6 @@
23 23 
24namespace ge {24namespace ge {
25namespace {25namespace {
26-constexpr int32_t kMaxStreamLimit = 64;
27- 
28const char_t *GetStrategyName(const minidag::StreamMergeStrategy strategy) {26const 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 
99Status RunMiniDAGStreamPassForComputGraph(const ConstGraphPtr &graph, StreamPassContext &context,51Status 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} // namespace52} // 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 
405Status SingleStreamPass::Run(ComputeGraphPtr graph, const std::vector<SubgraphPtr> &subgraphs, Context &context) {405Status 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#include "stream_utils.h"11#include "stream_utils.h"
12 12 
13+#include <algorithm>
14+#include <cctype>
15+ 
13#include "graph/utils/node_adapter.h"16#include "graph/utils/node_adapter.h"
14#include "register/custom_pass_helper.h"17#include "register/custom_pass_helper.h"
15#include "graph/utils/graph_utils_ex.h"18#include "graph/utils/graph_utils_ex.h"
@@ -23,6 +26,8 @@
23namespace {26namespace {
24constexpr const ge::char_t *const kTrueStr = "true";27constexpr const ge::char_t *const kTrueStr = "true";
25constexpr const ge::char_t *const kFalseStr = "false";28constexpr const ge::char_t *const kFalseStr = "false";
29+constexpr const ge::char_t *const kAutoMultistreamParallelModeOption = "ge.autoMultistreamParallelMode";
30+constexpr int32_t kMaxAutoMultistreamNum = 64;
26const std::set<std::string> hccl_op_types({ge::HCOMBROADCAST, ge::HCOMALLGATHER, ge::HCOMALLREDUCE,31const 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 {
32std::mutex StreamUtils::mutex_;37std::mutex StreamUtils::mutex_;
33std::map<std::string, PriorityEnum> StreamUtils::engine_priority_;38std::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+ 
35Status StreamUtils::ConvertSubgraphs(const ComputeGraphPtr &graph, const Graph2SubGraphInfoList &subgraph_map,50Status 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+}
夏国正20 天前

此条代码评论区间+282+299

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

likedislike
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;
25constexpr int64_t kMainStream = 0;25constexpr int64_t kMainStream = 0;
26constexpr int64_t kDefaultMaxParalleNum = 1;26constexpr 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+ 
28struct Subgraph {47struct 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在图上没有通路则可以并发
127void MarkCvParallelAivNodes(const ComputeGraphPtr &graph) {127void 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+#include "graph/attr_value.h"
12+#include "register/register_custom_pass.h"
13+ 
14+#include <cstdlib>
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+@dataclass(frozen=True)
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+@dataclass
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+ @property
94+ def valid(self) -> bool:
95+ return not self.reasons
96+ 
97+ 
98+@dataclass
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+ @property
112+ def destination(self) -> str:
113+ return "{}@{}".format(self.user, self.host)
114+ 
115+ 
116+@dataclass
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 
752const std::string ATTR_MODEL_HUGE_STREAM_LIST = "huge_stream_list";752const 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+ 
754const std::string ATTR_MODEL_LABEL_NUM = "label_num";756const std::string ATTR_MODEL_LABEL_NUM = "label_num";
755 757 
756const std::string ATTR_MODEL_MEMORY_SIZE = "memory_size";758const 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 
35namespace {35namespace {
36+constexpr const char_t *const kMiniDagStreamPass = "MiniDAGStreamPass";
37+constexpr const char_t *const kAutoMultistreamParallelModeOption = "ge.autoMultistreamParallelMode";
38+ 
36std::string CustomPassStageToString(CustomPassStage stage) {39std::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 &reg_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+ 
41Status RunAllocateStreamPass(const PassRegistrationData &reg_data, const GraphPtr &graph,82Status RunAllocateStreamPass(const PassRegistrationData &reg_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 &reg_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_;
夏国正20 天前

为什么涉及对外头文件修改

likedislike
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 
733GE_FUNC_DEV_VISIBILITY GE_FUNC_HOST_VISIBILITY extern const std::string ATTR_MODEL_HUGE_STREAM_LIST;733GE_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+ 
735GE_FUNC_DEV_VISIBILITY GE_FUNC_HOST_VISIBILITY extern const std::string ATTR_MODEL_LABEL_NUM;737GE_FUNC_DEV_VISIBILITY GE_FUNC_HOST_VISIBILITY extern const std::string ATTR_MODEL_LABEL_NUM;
736 738 
737GE_FUNC_DEV_VISIBILITY GE_FUNC_HOST_VISIBILITY extern const std::string ATTR_MODEL_MEMORY_SIZE;739GE_FUNC_DEV_VISIBILITY GE_FUNC_HOST_VISIBILITY extern const std::string ATTR_MODEL_MEMORY_SIZE;
@@ -25,6 +25,8 @@ set(EXECUTOR_SRC_LIST
25set(DAVINCI_SRC_LIST25set(DAVINCI_SRC_LIST
26 common/runtime/model_rt_var_manager.cc26 common/runtime/model_rt_var_manager.cc
27 common/runtime/rt_session.cc27 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.cc30 common/dump/opdebug_register.cc
29 common/dump/data_dumper.cc31 common/dump/data_dumper.cc
30 common/runtime_api_wrapper.cc32 common/runtime_api_wrapper.cc
@@ -361,6 +363,7 @@ target_compile_options(davinci_executor PRIVATE ${AIR_COMMON_DYNAMIC_COMPILE_OPT
361target_compile_definitions(davinci_executor PRIVATE363target_compile_definitions(davinci_executor PRIVATE
362 PROTOBUF_INLINE_NOT_IN_HEADERS=0364 PROTOBUF_INLINE_NOT_IN_HEADERS=0
363 google=ascend_private365 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+#include "common/multi_stream_tuning/model_tuning_config.h"
12+ 
13+#include "common/model/ge_model.h"
14+#include "common/model/ge_root_model.h"
15+#include "graph/debug/ge_attr_define.h"
16+#include "graph/utils/attr_utils.h"
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+#ifndef GE_BASE_COMMON_MULTI_STREAM_TUNING_MODEL_TUNING_CONFIG_H_
12+#define GE_BASE_COMMON_MULTI_STREAM_TUNING_MODEL_TUNING_CONFIG_H_
13+ 
14+#include <memory>
15+#include <string>
16+ 
17+#include "common/ge_visibility.h"
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+#endif // GE_BASE_COMMON_MULTI_STREAM_TUNING_MODEL_TUNING_CONFIG_H_
@@ -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+#include "common/multi_stream_tuning/step_recorder.h"
12+ 
13+#include <atomic>
14+#include <cctype>
15+#include <cinttypes>
16+#include <limits>
17+#include <map>
18+#include <mutex>
19+ 
20+#include "acl/acl_rt.h"
21+#include "common/ge_common/debug/ge_log.h"
22+#include "graph/ge_context.h"
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+#ifndef GE_BASE_COMMON_MULTI_STREAM_TUNING_STEP_RECORDER_H_
12+#define GE_BASE_COMMON_MULTI_STREAM_TUNING_STEP_RECORDER_H_
13+ 
14+#include <cstdint>
15+#include <string>
16+ 
17+#include "ge/ge_error_codes.h"
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+#endif // GE_BASE_COMMON_MULTI_STREAM_TUNING_STEP_RECORDER_H_
@@ -19,6 +19,8 @@
19#include "common/utils/executor_utils.h"19#include "common/utils/executor_utils.h"
20#include "common/runtime_api_wrapper.h"20#include "common/runtime_api_wrapper.h"
21#include "common/aclrt_malloc_helper.h"21#include "common/aclrt_malloc_helper.h"
22+#include "common/multi_stream_tuning/model_tuning_config.h"
23+#include "common/multi_stream_tuning/step_recorder.h"
22#include "framework/common/runtime_tensor_desc.h"24#include "framework/common/runtime_tensor_desc.h"
23#include "rt_error_codes.h"25#include "rt_error_codes.h"
24#include "common/file_constant_utils/file_constant_utils.h"26#include "common/file_constant_utils/file_constant_utils.h"
@@ -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 manager7744 // 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 manager7854 // 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::Init1551 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::Init1554 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#include "exe_graph/lowering/buffer_pool.h"28#include "exe_graph/lowering/buffer_pool.h"
29#include "common/checker.h"29#include "common/checker.h"
30#include "common/compile_profiling/ge_call_wrapper.h"30#include "common/compile_profiling/ge_call_wrapper.h"
31+#include "common/multi_stream_tuning/model_tuning_config.h"
32+#include "common/multi_stream_tuning/step_recorder.h"
31#include "runtime/subscriber/executor_subscribers_scheduler.h"33#include "runtime/subscriber/executor_subscribers_scheduler.h"
32#include "graph/utils/node_utils.h"34#include "graph/utils/node_utils.h"
33#include "core/executor/sequential/execution_data/sequential_execution_data_builder.h"35#include "core/executor/sequential/execution_data/sequential_execution_data_builder.h"
@@ -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#include "builder/model_v2_executor_builder.h"17#include "builder/model_v2_executor_builder.h"
18#include "register/kernel_registry.h"18#include "register/kernel_registry.h"
19#include "common/model/ge_root_model.h"19#include "common/model/ge_root_model.h"
20+#include "common/multi_stream_tuning/step_recorder.h"
20#include "core/utils/rt2_tensor_utils.h"21#include "core/utils/rt2_tensor_utils.h"
21#include "executor_error_code.h"22#include "executor_error_code.h"
22#include "framework/runtime/gert_const_types.h"23#include "framework/runtime/gert_const_types.h"
@@ -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}
252ge::graphStatus ModelV2Executor::ExecuteSync(Tensor **inputs, size_t input_num, Tensor **outputs, size_t output_num) {258ge::graphStatus ModelV2Executor::ExecuteSync(Tensor **inputs, size_t input_num, Tensor **outputs, size_t output_num) {
夏国正16 天前

ExecuteSync是不是没人调用,如果有调用的话,这里不需要打点吗,而且得加上流同步再打点

likedislike
KenChow
16 天前 评论:
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 
114add_dependencies(graph_engine_test115add_dependencies(graph_engine_test
@@ -155,6 +156,7 @@ target_link_libraries(graph_engine_test
155 ge_runner_v2156 ge_runner_v2
156 om2_executor157 om2_executor
157 ge_common_base158 ge_common_base
159+ davinci_executor
158 custom_op_runtime160 custom_op_runtime
159 gert161 gert
160 cpu_compiler162 cpu_compiler
@@ -26,6 +26,7 @@
26#include "op_impl/data_flow_op_impl.h"26#include "op_impl/data_flow_op_impl.h"
27#include "lowering/model_converter.h"27#include "lowering/model_converter.h"
28#include "securec.h"28#include "securec.h"
29+#include "graph/debug/ge_attr_define.h"
29#include "graph/utils/op_desc_utils.h"30#include "graph/utils/op_desc_utils.h"
30#include "graph/ge_context.h"31#include "graph/ge_context.h"
31#include "check/executor_statistician.h"32#include "check/executor_statistician.h"
@@ -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自带的Frame709 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 arg746 // 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 ge853} // 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+#include <gtest/gtest.h>
12+ 
13+#include <cstring>
14+#include <map>
15+#include <sstream>
16+#include <string>
17+#include <thread>
18+#include <vector>
19+ 
20+#include "common/multi_stream_tuning/step_recorder.h"
21+#include "stub/gert_runtime_stub.h"
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_static779 custom_op_registry_static
779 graph780 graph
780 graph_base781 graph_base
782+ ge_common_base
781 ge_executor_shared783 ge_executor_shared
782 hybrid_executor784 hybrid_executor
783 davinci_executor785 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+#include <gtest/gtest.h>
12+ 
13+#include <cstdarg>
14+#include <cstdint>
15+#include <cstdio>
16+#include <memory>
17+#include <mutex>
18+#include <string>
19+#include <thread>
20+#include <vector>
21+ 
22+#include "common/multi_stream_tuning/step_recorder.h"
23+#include "graph/ge_context.h"
24+#include "depends/ascendcl/src/ascendcl_stub.h"
25+#include "depends/slog/src/slog_stub.h"
26+#include "securec.h"
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#include "graph/build/stream/dag_stream_allocator_pass.h"19#include "graph/build/stream/dag_stream_allocator_pass.h"
20#include "graph/build/stream/dag_adapter.h"20#include "graph/build/stream/dag_adapter.h"
21+#include "graph/build/stream/stream_utils.h"
21#include "framework/common/ge_inner_error_codes.h"22#include "framework/common/ge_inner_error_codes.h"
22#include "external/graph/graph.h"23#include "external/graph/graph.h"
23#include "external/graph/operator.h"24#include "external/graph/operator.h"
@@ -25,6 +26,8 @@
25#include "graph/ge_context.h"26#include "graph/ge_context.h"
26#include "graph/ge_local_context.h"27#include "graph/ge_local_context.h"
27#include "register/optimization_option_registry.h"28#include "register/optimization_option_registry.h"
29+#include "graph/utils/attr_utils.h"
30+#include "graph/utils/graph_utils_ex.h"
28 31 
29namespace minidag {32namespace 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} // namespace130} // namespace
122 131 
123class DagStreamAllocatorPassTest : public testing::Test {132class 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+ 
641TEST_F(UtestLogicalStreamAllocator, test_single_stream_and_auto_multi_stream_conflict_precedes_stream_label) {660TEST_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#include "macro_utils/dt_public_scope.h"16#include "macro_utils/dt_public_scope.h"
17#include "ge_graph_dsl/graph_dsl.h"17#include "ge_graph_dsl/graph_dsl.h"
18#include "common/context/local_context.h"18#include "common/context/local_context.h"
19+#include "common/model/ge_root_model.h"
20+#include "common/multi_stream_tuning/model_tuning_config.h"
19#include "common/share_graph.h"21#include "common/share_graph.h"
20#include "graph/anchor.h"22#include "graph/anchor.h"
21#include "graph/attr_value.h"23#include "graph/attr_value.h"
@@ -510,6 +512,37 @@ void SetTbeKernelAttrs(const OpDescPtr &op_desc, const std::string &kernel_name,
510}512}
511} // namespace513} // 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+ 
513TEST_F(UtestModelBuilderTest, SaveDataToModelUsesLastBinForSameKernelNameWithDifferentBins) {546TEST_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#include <cstdlib>13#include <cstdlib>
14#include <string>14#include <string>
15+#include <tuple>
16+#include <vector>
15 17 
16#include "graph/build/stream/stream_utils.h"18#include "graph/build/stream/stream_utils.h"
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 ge164} // namespace ge
@@ -18,6 +18,8 @@
18#include "depends/profiler/src/profiling_auto_checker.h"18#include "depends/profiler/src/profiling_auto_checker.h"
19 19 
20#include "graph/utils/graph_utils.h"20#include "graph/utils/graph_utils.h"
21+#include "graph/debug/ge_attr_define.h"
22+#include "graph/utils/attr_utils.h"
21#include "graph/utils/op_desc_utils_ex.h"23#include "graph/utils/op_desc_utils_ex.h"
22#include "common/profiling/profiling_manager.h"24#include "common/profiling/profiling_manager.h"
23#include "framework/common/runtime_model_ge.h"25#include "framework/common/runtime_model_ge.h"
@@ -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+ 
174void TestNnExecuteWithGertTensor() {187void 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#include "exe_graph/runtime/tiling_context.h"21#include "exe_graph/runtime/tiling_context.h"
22#include "stub/gert_runtime_stub.h"22#include "stub/gert_runtime_stub.h"
23#include "framework/runtime/executor_option/multi_thread_executor_option.h"23#include "framework/runtime/executor_option/multi_thread_executor_option.h"
24+#include "graph/utils/attr_utils.h"
24#include "graph/utils/graph_dump_utils.h"25#include "graph/utils/graph_dump_utils.h"
25#include "kernel/common_kernel_impl/tiling.h"26#include "kernel/common_kernel_impl/tiling.h"
26#include "graph/debug/ge_attr_define.h"27#include "graph/debug/ge_attr_define.h"
@@ -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+ 
103TEST_F(ModelV2ExecutorBuilderUT, RefsHasTheSameAddr) {129TEST_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#include <gtest/gtest.h>11#include <gtest/gtest.h>
12#include <climits>12#include <climits>
13+#include <cstdlib>
13#include <fstream>14#include <fstream>
14 15 
15#include "register/register_custom_pass.h"16#include "register/register_custom_pass.h"
@@ -18,11 +19,19 @@
18#include "register/custom_pass_helper.h"19#include "register/custom_pass_helper.h"
19#include "depends/mmpa/src/mmpa_stub.h"20#include "depends/mmpa/src/mmpa_stub.h"
20#include "graph/ge_local_context.h"21#include "graph/ge_local_context.h"
22+#include "graph/utils/graph_utils_ex.h"
21 23 
22namespace ge {24namespace ge {
23namespace {25namespace {
24const char *const kEnvName = "ASCEND_OPP_PATH";26const 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
26class UtestRegisterPass : public testing::Test {35class 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+ 
322TEST_F(UtestRegisterPass, CustomPassContext_GetOptionValue) {392TEST_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();