Tiling implementation on the host has been described in section "SIMD Operator Implementation". This section focuses on the programming mode and API usage when the CANN framework is connected.
In most cases, the local memory cannot completely store the operator input and output. Therefore, you need to move some inputs for computation multiple times until the complete final result is obtained. This process of data tiling and block computation is called tiling. Tiling implementation is a compute program that determines parameters related to the data tiling algorithm (such as the block size to be moved each time and the total number of cycles) based on information such as the shape of an operator.
After tiling is implemented, the obtained parameters related to the tiling algorithm are passed to the kernel to guide parallel data tiling. The AI Core is not good at scalar computation in the tiling implementation. Therefore, the AI Core is executed on the host CPU independently.
As shown in the preceding figure, tiling implementation is a process of determining parameters related to the tiling algorithm based on information such as the operator shape. The information such as the operator shape can be considered as the input of tiling implementation, and the parameters related to the tiling algorithm can be considered as the output of tiling implementation. The input and output are carried by the parameters (TilingContext* context structure) of the tiling function. You can obtain the input of tiling implementation: input, output, and attribute information of the operator from the context structure. After tiling, you can obtain the output of the tiling implementation: TilingData structure (parameters related to the tiling algorithm), numBlocks variable, TilingKey that is used to select different kernel implementation branches, and operator workspace, and then set the output to the context structure.
The concepts of TilingData, numBlocks, TilingKey, and workspace are described as follows:
The TilingData structure can be defined in a single structure or nested structures.
1 2 3 4 5 6 7 8 9 |
namespace optiling { BEGIN_TILING_DATA_DEF(MyAddTilingData) // Declare the name of the tiling structure. TILING_DATA_FIELD_DEF(uint32_t, field1); // Types and names of structure members. TILING_DATA_FIELD_DEF(uint32_t, field2); TILING_DATA_FIELD_DEF(uint32_t, field3); END_TILING_DATA_DEF; REGISTER_TILING_DATA_CLASS(MyAdd, MyAddTilingData) // Register the tiling structure with the operator. } |
The method of assigning values to tiling structure members in the tiling implementation function is as follows:
1 2 3 |
MyAddTilingData myTiling; myTiling.set_field1(1); myTiling.set_field2(2); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
namespace optiling { BEGIN_TILING_DATA_DEF(MyStruct1) // Declare the name of structure 1. TILING_DATA_FIELD_DEF(uint32_t, field1); // Types and names of structure members. TILING_DATA_FIELD_DEF(uint32_t, field2); // Types and names of structure members. END_TILING_DATA_DEF; REGISTER_TILING_DATA_CLASS(MyStruct1Op, MyStruct1) // Register the structure with <op_type>Op. BEGIN_TILING_DATA_DEF(MyStruct2) // Declare the name of structure 2. TILING_DATA_FIELD_DEF(uint32_t, field3); // Types and names of structure members. TILING_DATA_FIELD_DEF(uint32_t, field4); // Types and names of structure members. END_TILING_DATA_DEF; REGISTER_TILING_DATA_CLASS(MyStruct2Op, MyStruct2) // Register the structure with <op_type>Op. BEGIN_TILING_DATA_DEF(MyAddTilingData) // Declare the name of the tiling structure. TILING_DATA_FIELD_DEF_STRUCT(MyStruct1, st1); // Reference structure of the structure member. TILING_DATA_FIELD_DEF_STRUCT(MyStruct2, st2); // Reference structure of the structure member. END_TILING_DATA_DEF; REGISTER_TILING_DATA_CLASS(MyAdd, MyAddTilingData) // Register the tiling structure with the operator. } |
The method of assigning values to tiling structure members in the tiling implementation function is as follows:
1 2 3 4 5 |
MyAddTilingData myTiling; myTiling.st1.set_field1(1); myTiling.st1.set_field2(2); myTiling.st2.set_field3(3); myTiling.st2.set_field4(4); |
numBlocks is a concept about logical cores, and its value range is [1, 65535]. To fully utilize hardware resources, set this parameter to the number of physical cores or a multiple of the number of physical cores.
Assume that the following kernel code exists:
1 2 3 4 5 |
if (condition) { ProcessA(); } else { ProcessB(); } |
If ProcessA and ProcessB are large functions, the preceding code will be larger after compilation. However, only one selection is chosen each time the kernel runs. The condition judgment and jump are as large as 16 KB to 32 KB, which varies with chips. Then, iCache miss occurs. You can use TilingKey to set different TilingKey 1 and TilingKey 2 for the processing functions of the two kernels.
1 2 3 4 5 |
if (TILING_KEY_IS(1)) { ProcessA(); } else if (TILING_KEY_IS(2)) { ProcessB(); } |
In this way, the device kernel automatically identifies two TilingKeys during compilation, compiles two kernel entrypoint functions, and folds constants for condition judgment. In addition, this function needs to work with the host tiling function to set TilingKey to 1 for the scenario where ProcessA is used and set TilingKey to 2 for the scenario where ProcessB is used.
1 2 3 4 5 6 7 8 9 10 |
static ge::graphStatus TilingFunc(gert::TilingContext* context) { // some code if (condition) { context->SetTilingKey(1); } else { context->SetTilingKey(2); } return ge::GRAPH_SUCCESS; } |
You can set --tiling_key to specify TilingKey, and compile the kernel code related to the TilingKey, to accelerate compilation.
The workspace memory consists of two parts: workspace memory required by Ascend C APIs and workspace memory used by operator implementation (on-demand).
During API compute, some workspace memory is required as the cache. Therefore, the operator tiling function needs to reserve workspace memory for the API. The reserved memory size can be obtained by calling GetLibApiWorkSpaceSize.
The memory needs to be allocated only when extra device memory is required for data exchange or caching in the operator. The memory is allocated based on the space computed by the operator.
The overall workspace memory is the sum of the preceding two parts. To set the workspace memory in the tiling function, perform the following steps:
1 2 |
auto workspaceSizes = context->GetWorkspaceSizes(1); // Use only one workspace. workspaceSizes[0] = sysWorkspaceSize + usrWorkspaceSize; |
The following figure shows the development process of tiling.
The following uses a simple Add operator as an example to describe the implementation process of tiling. In this sample, the shape size of the data to be processed can be evenly allocated to each core and aligned to the size of a data block (32 bytes).
Compile the header file for defining the TilingData structure of the operator. The file is named operator name_tiling.h and stored in the op_host directory of the operator project. The sample code is as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#ifndef ADD_CUSTOM_TILING_H #define ADD_CUSTOM_TILING_H #include "register/tilingdata_base.h" namespace optiling { BEGIN_TILING_DATA_DEF(TilingData) // Register a tiling class and uses the tiling name as the input parameter. TILING_DATA_FIELD_DEF(uint32_t, totalLength); // Add the tiling field to compute the total data volume. TILING_DATA_FIELD_DEF(uint32_t, tileNum); // Add the tiling field that specifies the total number of data blocks to be computed on each core. END_TILING_DATA_DEF; // Register the operator TilingData class with the corresponding AddCustom operator. REGISTER_TILING_DATA_CLASS(AddCustom, TilingData) } #endif // ADD_CUSTOM_TILING_H |
The procedure is as follows:
1 2 3 4 5 6 7 8 9 |
#ifndef ADD_CUSTOM_TILING_H #define ADD_CUSTOM_TILING_H #include "register/tilingdata_base.h" namespace optiling { // Tiling structure definition and registration code // ... } #endif // ADD_CUSTOM_TILING_H |
1 2 3 4 |
BEGIN_TILING_DATA_DEF(TilingData) // Register a tiling class and uses the tiling name as the input parameter. TILING_DATA_FIELD_DEF(uint32_t, totalLength); // Add the tiling field to compute the total data volume. TILING_DATA_FIELD_DEF(uint32_t, tileNum); // Add the tiling field that specifies the total number of data blocks to be computed on each core. END_TILING_DATA_DEF; |
1 2 |
// Register the operator TilingData class with the corresponding AddCustom operator. REGISTER_TILING_DATA_CLASS(AddCustom, TilingData) |
Implement the tiling function in the .cpp file of the operator host. The file is named Operator name.cpp and stored in the op_host directory of the operator project. The prototype of the tiling function is definite. It accepts a TilingContext as the input. The input and output shape pointers can be obtained from the context. The registered tiling function is called by the framework, and the TilingContext parameter is passed during the call. The sample code is as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
namespace optiling { const uint32_t NUM_BLOCKS = 8; const uint32_t TILE_NUM = 8; static ge::graphStatus TilingFunc(gert::TilingContext *context) { TilingData tiling; uint32_t totalLength = context->GetInputShape(0)->GetOriginShape().GetShapeSize(); context->SetBlockDim(NUM_BLOCKS); tiling.set_totalLength(totalLength); tiling.set_tileNum(TILE_NUM); tiling.SaveToBuffer(context->GetRawTilingData()->GetData(), context->GetRawTilingData()->GetCapacity()); context->GetRawTilingData()->SetDataSize(tiling.GetDataSize()); size_t *currentWorkspace = context->GetWorkspaceSizes(1); currentWorkspace[0] = 0; return ge::GRAPH_SUCCESS; } } // namespace optiling |
The procedure is as follows:
1 2 |
// Obtain the input shape information. uint32_t totalLength = context->GetInputShape(0)->GetOriginShape().GetShapeSize(); |
1 2 3 4 5 |
// Use TilingData to define a specific instance. TilingData tiling; // Set TilingData. tiling.set_totalLength(totalLength); tiling.set_tileNum(TILE_NUM); |
1 2 3 |
// Serialize and save the file. tiling.SaveToBuffer(context->GetRawTilingData()->GetData(), context->GetRawTilingData()->GetCapacity()); context->GetRawTilingData()->SetDataSize(tiling.GetDataSize()); |
1
|
context->SetBlockDim(NUM_BLOCKS); |
1
|
context->SetTilingKey(1); |
1 2 |
size_t *currentWorkspace = context->GetWorkspaceSizes(1); currentWorkspace[0] = 0; |