ST用例开发指导
返回 DT用例开发总纲
ST的测试范围是一个特性,从大模块角度(例如编译时、执行时)使能和测试某个特性,并观察编译/执行的结果是否符合预期。ST用例与STC对应,因此相对于UT用例,ST用例更正式一些,对应地数量也更少一些。
ST用例的注释要求
每个ST都需要对应一个STC设计,ST的注释便是STC对用例的描述。在需求设计阶段,STC通过excel等方式承载,编码完成后,STC的设计通过ST用例的注释来承载,后续变更、新增ST用例均同步修改对应注释,STC的原始excel文件不再维护。ST用例的注释中,需要包含四部分内容:
- 用例描述:一句描述本用例的测试场景
- 预置条件:如何构造ST环境,fake、打桩哪些基础环境数据
- 测试步骤:简要描述测试的步骤
- 预期结果:期望得到什么样的结果,如果有多条,那么分条编写
举例说明,比如要测试If节点在rt2的执行是否正常,可能有如下的ST用例注释:
/**
* 用例描述:测试If算子中,当cond输入为一个scalar,并且为1时,进入then分支执行
* 预置条件:
* 1. Fake Shape和Rank算子的lowering和执行kernel
* 测试步骤:
* 1. 构造一张If图,then分支中包含一个Shape节点,else分支包含一个Rank节点
* 2. lowering、加载这张If图
* 3. 挂接ESS,监控执行图的执行情况
* 4. 构造两个Data,一个为scalar值1,传递为cond;另一个为Tensor,shape是{1,1}
* 5. 执行
* 预期结果:
* 1. 网络输出结果是{2}
* 2. 通过ess观察可知,Rank未被执行,Shape被执行
*/
ST用例场景设计checklist
| 序号 | 场景 | 说明 |
|---|---|---|
| 1 | 空/scalar Tensor | 理论上每个特性都会面临空Tensor的影响,空Tensor包含输出空Tensor和输入空Tensor两种场景。对空Tensor的覆盖测试,大部分应该在UT阶段完成。但是UT为单点测试,并不能代表整个特性对空Tensor的支持度。因此稳妥起见,若与空tensor相关,ST补充一个用例是值得的。 |
| 2 | 可维(Profiling、DataDump、ExceptionDump) | 如果某个特性对执行图做了变更,那么可能会影响到可维特性(可维特性是基于执行图的某些特征做的)。当前我们正在设计如何让新特性的开发者无感知地自动测试可维特性,但是在这个测试能力上线前,新特性的开发者仍然需要评估自己新增的特性是否影响了可维。如果不确定,可以简单地增加一个对应用例。 |
如何校验
图编译校验
图编译过程从流程上来看分为图准备、图优化、图编译。前两个阶段主要对图做修改,最后一个阶段完成流分配、生成task、内存分配(若为静态模型)等操作,某些关键属性也会保存在图上。
图的校验
图优化的过程中会阶段性地dump图,可以通过dump某个阶段图对图上节点、连边关系、属性等校验。配对使用DUMP_GRAPH_WHEN、CHECK_GRAPH这两个宏。第一个宏指定了dump哪个阶段的图,第二个宏里可以写图校验逻辑。
/**
*
* Variable(2, 3, 4, 5) Relu3(1,2,3,4,5)
* / \ / \
* TransData TransData TransData summary
* | | |
* Relu(1,2,3,4,5) Relu(1,2,3,4,5) Variable(2, 3, 4, 5)
* \ / |
* NetOutput -------------------------------
*
* 用例描述:经过Summary优化,Summary算子被删除,其输入节点作为整网输出连接到Netoutput上
* 预置条件:
* 1. 注册Summary算子的图
* 测试步骤:
* 1. 构造一张带有Summary算子的图
* 2. 指定DUMP PreRunAfterHandleSummaryOp 这张图
* 3. 构造Session,执行AddGraph和RunGraph接口,执行图编译操作
* 4. 针对 PreRunAfterHandleSummaryOp 这张图做校验
* 预期结果:
* 1. PreRunAfterHandleSummaryOp这张图上,Summary算子被删除
* 2. Netoutput的输出由2个变成了3个。Relu3作为输出连到了Netoutput上
* 3. SummaryCallback函数被调用了1次
*/
TEST_F(SummaryAndCheckPointTest, test_optimize_summary_graph) {
// build graph
Graph graph = BuildVariableGraph();
// new session & add graph
map<AscendString, AscendString> options;
options[ge::OPTION_GRAPH_RUN_MODE] = AscendString("1"); // train flag
options[VARIABLE_MEMORY_MAX_SIZE] = "12800";
Session session(options);
uint32_t graph_id = 1;
auto ret = session.AddGraph(graph_id, graph, options);
EXPECT_EQ(ret, SUCCESS);
// register Summary call back func
static uint32_t called_count = 0;
ge::session::pCallBackFunc callbackFuncSummary = [](uint32_t graph_id, const std::map<AscendString, ge::Tensor> ¶ms_list) {
called_count++;
return SUCCESS;
};
ret = session.RegisterCallBackFunc("Summary", callbackFuncSummary);
EXPECT_EQ(ret, SUCCESS);
// build input tensor
std::vector<Tensor> inputs;
Shape shape({1, 1, 224, 224});
TensorDesc tensor_desc(shape, FORMAT_NCHW, DT_FLOAT);
std::vector<uint8_t> data(224 * 224 * sizeof(float));
Tensor tensor(tensor_desc, data);
inputs.emplace_back(tensor);
std::vector<Tensor> outputs;
// build_graph through session
DUMP_GRAPH_WHEN("PreRunAfterHandleSummaryOp");
ret = session.RunGraph(graph_id, inputs, outputs);
EXPECT_EQ(ret, SUCCESS);
// check result
CHECK_GRAPH(PreRunAfterHandleSummaryOp) {
auto summary = graph->FindNode("summary");
ASSERT_EQ(summary, nullptr); // summary has been deleted
auto netoutput = graph->FindNode("output");
ASSERT_NE(netoutput, nullptr);
ASSERT_EQ(netoutput->GetInDataNodes().size(), 4); // add relu3 as input of netoutput
ASSERT_EQ(netoutput->GetInDataNodes().at(3)->GetName(), "relu3");
};
EXPECT_EQ(called_count, 1);
}
DUMP_GRAPH_WHEN 可用阶段列表
以下是在现有用例中实际使用的 DUMP_GRAPH_WHEN 阶段名称,编写ST用例时可根据需要选择合适的阶段捕获图快照:
| 分类 | 阶段名称 | 说明 |
|---|---|---|
| 图准备 | PrepareAfterUpdateInputOutputByUserOptions |
用户选项更新输入输出后 |
PrepareAfterInsertAipp |
AIPP插入后 | |
PrepareAfterProcessAippStage2 |
AIPP处理第二阶段后 | |
PrepareAfterCheckAndUpdateInput |
检查并更新输入后 | |
PrepareAfterInferFormatAndShape |
推断格式和Shape后 | |
PrepareAfterPrepareOptimize |
准备优化后 | |
| 图优化 | PreRunBegin |
PreRun开始 |
PreRunAfterNormalizeGraph |
图规范化后 | |
PreRunAfterHandleSummaryOp |
Summary算子处理后 | |
PreRunAfterOptimizeSubgraph |
子图优化后 | |
PreRunAfterOptimize1 |
第一轮优化后 | |
PreRunAfterOptimize2 |
第二轮优化后 | |
PreRunAfterPrepareRunningFormatRefiner |
运行时格式精炼后 | |
PreRunAfterOptimizeGraphBeforeBuild |
构建前图优化后 | |
PreRunAfterInitPreparation |
初始化准备后 | |
PreRunAfterMemConflictProc |
内存冲突处理后 | |
PreRunAfterPrepare |
准备完成后 | |
PreRunAfterBuild |
构建完成后 | |
OptimizeStage1_1 / OptimizeStage1_2 |
优化阶段1 | |
OptimizeOriginalGraph_FeGraphFusionAfter |
FE图融合后 | |
OptimizeOriginalGraph_FeInsertTransNodeAfter |
FE插入TransNode后 | |
OptimizeOriginalGraph_FeDistHeavyFormatAfter |
FE分布式重格式后 | |
OptimizeQuantGraph_FeGraphFusionAfter |
量化图FE融合后 | |
OptimizeGraph_TagNoConstFoldingAfter |
标记无常量折叠后 | |
BeforeOptimizeOriginalGraphJudgeInsert |
原始图优化判断插入前 | |
AfterRecompute |
重计算后 | |
AfterDynamicShapePartition |
动态Shape分区后 | |
After_AutoFusePass |
自动融合Pass后 | |
AutoFuser_BeforeAutoFuse |
自动融合前 | |
| 图构建 | Build |
构建阶段 |
before_build / after_build |
构建前/后 | |
AfterAssignResource |
资源分配后 | |
AfterGetTask |
获取Task后 | |
AfterCalcOpParam |
算子参数计算后 | |
AfterInfershape |
推断Shape后 | |
AfterSwapSpace / BeforeSwapSpace |
SwapSpace处理 | |
MergedComputeGraphAfterCompositeEnginePartition |
复合引擎分区合并后 |
该列表来源于现有用例的实际使用,可能不完整。如果需要 dump 新的阶段,可以在生产代码中通过注册对应的 dump 阶段名来实现。
图执行校验
对于图执行,需要构造对应网络,根据不同的输入,产生不同的结果,ST用例直接校验网络的输出结果。
void RunIfGraph2(TensorHolder &pred_tensor, bool expect_branch) {
// 构造一张If图
auto compute_graph = ShareGraph::IfGraph2();
ASSERT_NE(compute_graph, nullptr);
compute_graph->TopologicalSorting();
GeModelBuilder builder(compute_graph);
auto ge_root_model = builder.BuildGeRootModel();
// 对Shape/Rank算子打桩,接管这两个算子的执行
GertRuntimeStub rtstub;
rtstub.StubByNodeTypes({"Shape", "Rank"});
// lowering
auto exe_graph = ModelConverter().ConvertGeModelToExecuteGraph(ge_root_model);
ASSERT_NE(exe_graph, nullptr);
auto model_executor = ModelV2Executor::Create(exe_graph, ge_root_model);
ASSERT_NE(model_executor, nullptr);
// 创建一个观察者,观察执行图的执行状况
auto ess = StartExecutorStatistician(model_executor);
// 加载
EXPECT_EQ(model_executor->Load(), ge::GRAPH_SUCCESS);
// 构造网络的输入stream和tensor
auto input_holder = TensorFaker().Placement(kOnDeviceHbm).DataType(ge::DT_FLOAT).Shape({8, 3, 224, 224}).Build();
std::vector<Tensor *> inputs{pred_tensor.GetTensor(), input_holder.GetTensor()};
auto output_holder = TensorFaker().Placement(kOnHost).DataType(ge::DT_INT64).Shape({8}).Build();
std::vector<Tensor *> outputs{output_holder.GetTensor()};
rtStream_t stream;
ASSERT_EQ(rtStreamCreate(&stream, static_cast<int32_t>(RT_STREAM_PRIORITY_DEFAULT)), RT_ERROR_NONE);
auto stream_value = FakeValue<uint64_t>(reinterpret_cast<uint64_t>(stream));
// 期望的网络输出值,因为这是个通用的测试函数,因此根据期望的true/false分支,而走shape或rank算子,进而会有不同的期望输出值
auto shape_count = expect_branch ? 1 : 0;
auto rank_count = expect_branch ? 0 : 1;
auto output_shape = expect_branch ? Shape({4}) : Shape({});
auto output_data = expect_branch ? Shape({8, 3, 224, 224}) : Shape({4});
// 执行
ess->Clear();
ASSERT_EQ(
model_executor->Execute({stream_value.value}, inputs.data(), inputs.size(), outputs.data(), outputs.size()),
ge::GRAPH_SUCCESS);
// 校验Shape/Rank节点的执行次数是否符合预期
EXPECT_EQ(ess->GetExecuteCountByNodeTypeAndKernelType("Shape", "FakeExecuteNode"), shape_count);
EXPECT_EQ(ess->GetExecuteCountByNodeTypeAndKernelType("Rank", "FakeExecuteNode"), rank_count);
EXPECT_EQ(output_holder.GetTensor()->GetShape().GetStorageShape(), output_shape);
EXPECT_EQ(output_holder.GetTensor()->GetShape().GetOriginShape(), output_shape);
EXPECT_EQ(memcmp(output_holder.GetTensor()->GetAddr(), &output_data[0], output_data.GetDimNum() * 8), 0);
// 清理资源
ASSERT_EQ(model_executor->UnLoad(), ge::GRAPH_SUCCESS);
rtStreamDestroy(stream);
}
两轮运行校验
有时一个业务会区分初始化和运行两种状态,例如:
- 图执行会区分图加载和图执行两个阶段,首次执行做加载、执行,二次及以后仅做执行
- 对于端到端的执行一张计算图,首次执行会经历编译、加载、执行三个阶段,二次执行会跳过编译、加载,仅做执行
对于此类业务,考虑对运行状态做两轮调用,对两轮调用的结果分别校验。
做两轮校验的原因是,对于此类业务,首轮运行时,系统处于初始化状态,首轮运行后,系统中可能存在脏数据,影响二轮执行的结果。做两轮调用可以有效校验脏数据问题。
// 第一次执行
ASSERT_EQ(model_executor->Execute({i3.value}, inputs.GetTensorList(), inputs.size(),
outputs.data(), outputs.size()),
ge::GRAPH_SUCCESS);
// 更多校验细节,这里省略
// 第二次执行
ASSERT_EQ(model_executor->Execute({i3.value}, inputs.GetTensorList(), inputs.size(),
outputs.data(), outputs.size()),
ge::GRAPH_SUCCESS);
// 与第一次执行结束后相同的细节校验
// 模型卸载和销毁
ASSERT_EQ(model_executor->UnLoad(), ge::GRAPH_SUCCESS);
rtStreamDestroy(stream);