ATB Graph Offloading
Applicable Products
Hardware |
Supported |
|---|---|
Atlas 350 accelerator card |
√ |
√ |
|
√ |
|
x |
|
x |
|
x |
Function Overview
The graph offloading feature delivers all device-side tasks in a graph operator (GraphOperation) to the device at once and solidifies them on the device side. Graph offloading is the same as single-operator delivery when a model instance is captured for the first time. In subsequent replay, if graph offloading is used, re-delivery is not required, and the tasks solidified on the device side are directly executed. In this way, the host is no longer required for replay after the capture is complete, greatly reducing the time consumed on the host and optimizing the global time consumption.
Functions
ATB's graph offloading supports two capture modes, external and internal.
- External capture
External capture indicates that you call aclmdlRI-related APIs except the setup and execute APIs in the operation of the ATB. For external capture, if the PreLaunch phase of the Execute API of the ATB is used to update parameters (variantPack and workspace), you need to call the SetLaunchMode API in Context to set the delivery mode to atb::GRAPH_LAUNCH_MODE.
The procedure is as follows.
Figure 1 External capture
- Model creation
Create a model running instance (model).
aclmdlRI model= nullptr
- Task capturing
Call the aclmdlRICaptureBegin and aclmdlRICaptureEnd APIs to determine the device-side tasks to be captured on the specified stream. All device-side tasks delivered on the specified stream are not executed immediately between the aclmdlRICaptureBegin and aclmdlRICaptureEnd APIs. Instead, they are temporarily stored in the internal model (aclmdlRI model) running instance of the system.
- aclmdlRICaptureBegin
Call the aclmdlRICaptureBegin API to start capturing the device-side tasks delivered on the stream.
- Delivery of tasks to be captured
Record the device-side tasks to be delivered in the model running instance by using ATB's graph delivery mode.
- Set the ATB delivery mode.
context->SetLaunchMode(atb::GRAPH_LAUNCH_MODE)
- Run Setup and Execute (PreLaunch).
- Set the ATB delivery mode.
- aclmdlRICaptureEnd
Call the aclmdlRICaptureEnd API to stop capturing the device-side tasks delivered on the stream and obtain the aclmdlRI model running instance.
- aclmdlRICaptureBegin
- Model execution
Execute all device-side tasks saved in the aclmdlRI model running instance during the capture phase.
- Subsequent replay
After the capture is complete, you only need to call Setup, PreLaunch, and aclmdlRIExecuteAsync if the ATB GraphOperation and input tensor remain unchanged. In the Setup phase, only some simple verification is performed. In the PreLaunch phase, data (workspace, operator Args, and tiling) is updated.
- Model creation
- Internal capture
In internal capture scenarios, the ATB encapsulates the aclmdlRI APIs. Users are unaware of the differences between single-operator delivery and graph offloading in the call procedure. The Setup and Execute (in the PreLaunch and Launch phases) APIs of the ATB are still used. The ATB creates models, captures tasks, and executes tasks. Compared with external capture, internal capture keeps the call logic of the ATB, making more convenient in use.
Figure 2 Internal capture
- Delivery mode setting
Call the SetLaunchMode API in the context class of the ATB to set the delivery mode of the ATB to graph delivery.
SetLaunchMode(atb::GRAPH_LAUNCH_MODE)
- Subsequent replay
After the capture is complete, you only need to repeatedly call the Setup and Execute APIs of the ATB if the ATB GraphOperation and input tensor remain unchanged. After the capture, all device-side tasks are solidified on the device side. Therefore, only some verification is performed in the Setup phase. In the Execute phase, data is updated and the graph is delivered, greatly reducing the time consumption on the host side in the corresponding phase.
- Delivery mode setting
Precautions
- If the device-side tasks captured in the capture phase change (the graph structure in the ATB GraphOperation changes) or the input tensor size changes, you need to capture the tasks again.
- The aclmdlRICaptureBegin and aclmdlRICaptureEnd APIs must be used in pairs, and the streams in the two APIs must be the same.
- All asynchronous device-side tasks are recorded in the model during the capture. Therefore, if a device-side memory problem occurs, you can check whether an asynchronous device-side task that should not be captured (for example, aclrtMemcpyAsync) is captured.
- Note that if a synchronous API (such as aclrtMalloc or aclrtMemcpy) needs to be called during the capture, you need to set mode in the aclmdlRICaptureBegin API to ACL_MODEL_RI_CAPTURE_MODE_RELAXED.
- Currently, only the tensor address and tiling content of some built-in operators can be updated. The workspace address, tensor shape, and other inputs cannot be updated.
- Currently, this feature is supported for Atlas 350 accelerator card, but the specific product support depends on the operator in use.
Examples
1 2 | source ${toolkit installation directory}/set_env.sh # For example, source /usr/local/Ascend/ascend-toolkit/set_env.sh source ${nnal installation directory}/atb/set_env.sh # For example, source /usr/local/Ascend/nnal/atb/set_env.sh |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 | #include <atb/operation.h> #include "atb/infer_op_params.h" #include <atb/types.h> #include <acl/acl.h> #include <iostream> #include <vector> #include <atb/utils.h> #include "experiment/runtime/runtime/rt_model.h" #include "experiment/runtime/runtime/stream.h" #include <unistd.h> #include <acl/acl_mdl.h> #include <cstdlib> // std::getenv included. #include <cstring> // std::strcmp included (used for string comparison) // Set the properties of each intensor. void CreateInTensorDescs(atb::SVector<atb::TensorDesc> &intensorDescs) { for (size_t i = 0; i < intensorDescs.size(); i++) { intensorDescs.at(i).dtype = ACL_FLOAT16; intensorDescs.at(i).format = ACL_FORMAT_ND; intensorDescs.at(i).shape.dimNum = 2; intensorDescs.at(i).shape.dims[0] = 2; intensorDescs.at(i).shape.dims[1] = 2; } } // Set each intensor and allocate memory space for each intensor. The intensor is manually set. In the project implementation, you can use torchTensor conversion or other simple data structure conversion. void CreateInTensors(atb::SVector<atb::Tensor> &inTensors, atb::SVector<atb::TensorDesc> &intensorDescs, uint32_t value) { for (size_t i = 0; i < inTensors.size(); i++) { inTensors.at(i).desc = intensorDescs.at(i); inTensors.at(i).dataSize = atb::Utils::GetTensorSize(inTensors.at(i)); std::vector<uint16_t> hostData(atb::Utils::GetTensorNumel(inTensors.at(i)), value); // A host buffer filled with all twos. int ret = aclrtMalloc(&inTensors.at(i).deviceData, inTensors.at(i).dataSize, ACL_MEM_MALLOC_HUGE_FIRST); // Allocate NPU memory. if (ret != 0) { std::cout << "alloc error!"; exit(0); } ret = aclrtMemcpy(inTensors.at(i).deviceData, inTensors.at(i).dataSize, hostData.data(), hostData.size() * sizeof(uint16_t), ACL_MEMCPY_HOST_TO_DEVICE); //Copy the CPU memory to the NPU. } } // Set each outtensor and allocate memory space for each outtensor. The setting is the same as that of an intensor. void CreateOutTensors(atb::SVector<atb::Tensor> &outTensors, atb::SVector<atb::TensorDesc> &outtensorDescs) { for (size_t i = 0; i < outTensors.size(); i++) { outTensors.at(i).desc = outtensorDescs.at(i); outTensors.at(i).dataSize = atb::Utils::GetTensorSize(outTensors.at(i)); int ret = aclrtMalloc(&outTensors.at(i).deviceData, outTensors.at(i).dataSize, ACL_MEM_MALLOC_HUGE_FIRST); if (ret != 0) { std::cout << "alloc error!"; exit(0); } } } // Build subgraphs. void CreateGraphOperation(atb::GraphParam &opGraph, atb::Operation **operation) { opGraph.inTensorNum = 4; opGraph.outTensorNum = 1; opGraph.internalTensorNum = 2; opGraph.nodes.resize(3); enum InTensorId { IN_TENSOR_A = 0, IN_TENSOR_B, IN_TENSOR_C, IN_TENSOR_D, ADD3_OUT, ADD1_OUT, ADD2_OUT }; size_t nodeId = 0; atb::Node &addNode = opGraph.nodes.at(nodeId++); atb::Node &addNode2 = opGraph.nodes.at(nodeId++); atb::Node &addNode3 = opGraph.nodes.at(nodeId++); atb::infer::ElewiseParam addParam; addParam.elewiseType = atb::infer::ElewiseParam::ElewiseType::ELEWISE_ADD; atb::Status status = atb::CreateOperation(addParam, &addNode.operation); addNode.inTensorIds = {IN_TENSOR_A, IN_TENSOR_B}; addNode.outTensorIds = {ADD1_OUT}; atb::infer::ElewiseParam addParam2; addParam2.elewiseType = atb::infer::ElewiseParam::ElewiseType::ELEWISE_ADD; status = atb::CreateOperation(addParam2, &addNode2.operation); addNode2.inTensorIds = {IN_TENSOR_C, IN_TENSOR_D}; addNode2.outTensorIds = {ADD2_OUT}; atb::infer::ElewiseParam addParam3; addParam3.elewiseType = atb::infer::ElewiseParam::ElewiseType::ELEWISE_ADD; status = CreateOperation(addParam3, &addNode3.operation); addNode3.inTensorIds = {ADD1_OUT, ADD2_OUT}; addNode3.outTensorIds = {ADD3_OUT}; status = atb::CreateOperation(opGraph, operation); } // Build graphs. void CreateMultiGraphOperation(atb::GraphParam &opGraph, atb::Operation **operation) { opGraph.inTensorNum = 4; opGraph.outTensorNum = 1; opGraph.internalTensorNum = 4; opGraph.nodes.resize(5); enum InTensorId { IN_TENSOR_A = 0, IN_TENSOR_B, IN_TENSOR_C, IN_TENSOR_D, ADD5_OUT, ADD1_INTER, ADD2_INTER, ADD3_INTER, ADD4_INTER }; size_t nodeId = 0; atb::Node &addGraphNode1 = opGraph.nodes.at(nodeId++); atb::Node &addGraphNode2 = opGraph.nodes.at(nodeId++); atb::Node &addGraphNode3 = opGraph.nodes.at(nodeId++); atb::Node &addGraphNode4 = opGraph.nodes.at(nodeId++); atb::Node &addGraphNode5 = opGraph.nodes.at(nodeId++); atb::GraphParam addGraphParam1; CreateGraphOperation(addGraphParam1, &addGraphNode1.operation); addGraphNode1.inTensorIds = {IN_TENSOR_A, IN_TENSOR_B, IN_TENSOR_C, IN_TENSOR_D}; addGraphNode1.outTensorIds = {ADD1_INTER}; atb::GraphParam addGraphParam2; CreateGraphOperation(addGraphParam2, &addGraphNode2.operation); addGraphNode2.inTensorIds = {IN_TENSOR_A, IN_TENSOR_B, IN_TENSOR_C, IN_TENSOR_D}; addGraphNode2.outTensorIds = {ADD2_INTER}; atb::GraphParam addGraphParam3; CreateGraphOperation(addGraphParam3, &addGraphNode3.operation); addGraphNode3.inTensorIds = {IN_TENSOR_A, IN_TENSOR_B, IN_TENSOR_C, IN_TENSOR_D}; addGraphNode3.outTensorIds = {ADD3_INTER}; atb::GraphParam addGraphParam4; CreateGraphOperation(addGraphParam4, &addGraphNode4.operation); addGraphNode4.inTensorIds = {IN_TENSOR_A, IN_TENSOR_B, IN_TENSOR_C, IN_TENSOR_D}; addGraphNode4.outTensorIds = {ADD4_INTER}; atb::GraphParam addGraphParam5; CreateGraphOperation(addGraphParam5, &addGraphNode5.operation); addGraphNode5.inTensorIds = {ADD1_INTER, ADD2_INTER, ADD3_INTER, ADD4_INTER}; addGraphNode5.outTensorIds = {ADD5_OUT}; atb::Status status = atb::CreateOperation(opGraph, operation); } // Print the result. void PrintOutTensorValue(atb::Tensor &outTensor) { std::vector<uint16_t> outBuffer(atb::Utils::GetTensorNumel(outTensor)); int ret = aclrtMemcpy(outBuffer.data(), outBuffer.size() * sizeof(uint16_t), outTensor.deviceData, outTensor.dataSize, ACL_MEMCPY_DEVICE_TO_HOST); if (ret != 0) { std::cout << "copy error!"; exit(0); } for (size_t i = 0; i < outBuffer.size(); i = i + 1) { std::cout << "out[" << i << "] = " << (uint32_t)outBuffer.at(i) << std::endl; } } int main() { // Use the environment variable ATB_CAPTURE_TYPE to determine whether the internal or external capture is used. // ATB_CAPTURE_TYPE=in: internal capture const char* captureType = std::getenv("ATB_CAPTURE_TYPE"); aclInit(nullptr); // Set a device ID, create a stream, create a context, and set the stream. uint32_t deviceId = 1; aclrtSetDevice(deviceId); aclrtStream exeStream = nullptr; aclrtCreateStream(&exeStream); atb::Context *context = nullptr; atb::CreateContext(&context); context->SetExecuteStream(exeStream); context->SetLaunchMode(atb::GRAPH_LAUNCH_MODE); // Create a graph operator. atb::Operation *operation = nullptr; atb::GraphParam opGraph; CreateMultiGraphOperation(opGraph, &operation); // Prepare the input and output tensors. atb::VariantPack pack; atb::SVector<atb::TensorDesc> intensorDescs; atb::SVector<atb::TensorDesc> outtensorDescs; uint32_t inTensorNum = opGraph.inTensorNum; uint32_t outTensorNum = opGraph.outTensorNum; inTensorNum = operation->GetInputNum(); outTensorNum = operation->GetOutputNum(); pack.inTensors.resize(inTensorNum); intensorDescs.resize(inTensorNum); CreateInTensorDescs(intensorDescs); outtensorDescs.resize(outTensorNum); pack.outTensors.resize(outTensorNum); operation->InferShape(intensorDescs, outtensorDescs); CreateOutTensors(pack.outTensors, outtensorDescs); // Initialize the workspace. uint64_t workspaceSize = 0; void *workSpace = nullptr; void *workSpace1 = nullptr; void *workSpace2 = nullptr; int ret = 0; // Graph execution // Internal capture if (!captureType || std::strcmp(captureType, "in") == 0) { std::cout << "start test capture graph inside" << std::endl; for (size_t i = 0; i < 10; i++) { CreateInTensors(pack.inTensors, intensorDescs, i + 1); if (i == 0) { // Capture device-side tasks. operation->Setup(pack, workspaceSize, context); if (workspaceSize != 0 && workSpace == nullptr) { ret = aclrtMalloc(&workSpace, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); if (ret != 0) { std::cout << "alloc error!"; exit(0); } } std::cout << "workspace:" << workSpace << ", workspaceSize:" << workspaceSize << std::endl; context->SetExecuteType(atb::EXECUTE_PRELAUNCH); operation->Execute(pack, (uint8_t*)workSpace, workspaceSize, context); context->SetExecuteType(atb::EXECUTE_LAUNCH); operation->Execute(pack, (uint8_t*)workSpace, workspaceSize, context); } else if (i > 3 && i < 6) { // Subsequent replay execution. A new workspace can be passed. operation->Setup(pack, workspaceSize, context); if (workspaceSize != 0 && workSpace1 == nullptr) { ret = aclrtMalloc(&workSpace1, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); if (ret != 0) { std::cout << "alloc error!"; exit(0); } } std::cout << "workspace1:" << workSpace1 << ", workspaceSize:" << workspaceSize << std::endl; context->SetExecuteType(atb::EXECUTE_PRELAUNCH); operation->Execute(pack, (uint8_t*)workSpace1, workspaceSize, context); context->SetExecuteType(atb::EXECUTE_LAUNCH); operation->Execute(pack, (uint8_t*)workSpace1, workspaceSize, context); } else if (i >= 6 && i < 8) { // Subsequent replay execution. A new workspace can be passed. operation->Setup(pack, workspaceSize, context); if (workspaceSize != 0 && workSpace2 == nullptr) { ret = aclrtMalloc(&workSpace2, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); if (ret != 0) { std::cout << "alloc error!"; exit(0); } } std::cout << "workSpace2:" << workSpace2 << ", workspaceSize:" << workspaceSize << std::endl; context->SetExecuteType(atb::EXECUTE_PRELAUNCH); operation->Execute(pack, (uint8_t*)workSpace2, workspaceSize, context); context->SetExecuteType(atb::EXECUTE_LAUNCH); operation->Execute(pack, (uint8_t*)workSpace2, workspaceSize, context); } else { // Subsequent replay execution. std::cout << "workspace:" << workSpace << ", workspaceSize:" << workspaceSize << std::endl; operation->Setup(pack, workspaceSize, context); context->SetExecuteType(atb::EXECUTE_PRELAUNCH); operation->Execute(pack, (uint8_t*)workSpace, workspaceSize, context); context->SetExecuteType(atb::EXECUTE_LAUNCH); operation->Execute(pack, (uint8_t*)workSpace, workspaceSize, context); } // Synchronize the stream and wait until the computation on the device is complete. ret = aclrtSynchronizeStream(exeStream); if (ret != 0) { std::cout << "sync error!" << std::endl; exit(0); } std::cout << "aclrtSynchronizeStream success!" << std::endl; // Print the values of the output tensors. PrintOutTensorValue(pack.outTensors.at(0)); } } else { // External capture std::cout << "start test capture graph outside" << std::endl; // Create a model running instance. aclmdlRI model = nullptr; for (size_t i = 0; i < 10; i++) { CreateInTensors(pack.inTensors, intensorDescs, i + 1); if (i == 0) { // Capture device-side tasks. // Start the capture. aclmdlRICaptureBegin(exeStream, ACL_MODEL_RI_CAPTURE_MODE_RELAXED); operation->Setup(pack, workspaceSize, context); if (workspaceSize != 0 && workSpace == nullptr) { ret = aclrtMalloc(&workSpace, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); if (ret != 0) { std::cout << "alloc error!"; exit(0); } } std::cout << "workspace:" << workSpace << ", workspaceSize:" << workspaceSize << std::endl; context->SetExecuteType(atb::EXECUTE_PRELAUNCH); operation->Execute(pack, (uint8_t*)workSpace, workspaceSize, context); context->SetExecuteType(atb::EXECUTE_LAUNCH); operation->Execute(pack, (uint8_t*)workSpace, workspaceSize, context); // Finish the capture. aclmdlRICaptureEnd(exeStream, &model); // Execute the model running instance. aclmdlRIExecuteAsync(model, exeStream); } else if (i > 3 && i < 6) { // Subsequent replay execution. A new workspace can be passed. // In the Setup phase, only the variantPack is verified. operation->Setup(pack, workspaceSize, context); if (workspaceSize != 0 && workSpace1 == nullptr) { ret = aclrtMalloc(&workSpace1, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); if (ret != 0) { std::cout << "alloc error!"; exit(0); } } std::cout << "workspace1:" << workSpace1 << ", workspaceSize:" << workspaceSize << std::endl; // Call PreLaunch to update the parameters of the model running instance. context->SetExecuteType(atb::EXECUTE_PRELAUNCH); operation->Execute(pack, (uint8_t*)workSpace1, workspaceSize, context); // Replay the model running instance. aclmdlRIExecuteAsync(model, exeStream); } else if (i >= 6 && i < 8) { // Subsequent replay execution. A new workspace can be passed. operation->Setup(pack, workspaceSize, context); if (workspaceSize != 0 && workSpace2 == nullptr) { ret = aclrtMalloc(&workSpace2, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); if (ret != 0) { std::cout << "alloc error!"; exit(0); } } std::cout << "workSpace2:" << workSpace2 << ", workspaceSize:" << workspaceSize << std::endl; context->SetExecuteType(atb::EXECUTE_PRELAUNCH); operation->Execute(pack, (uint8_t*)workSpace2, workspaceSize, context); aclmdlRIExecuteAsync(model, exeStream); } else { // Subsequent replay execution. std::cout << "workspace:" << workSpace << ", workspaceSize:" << workspaceSize << std::endl; operation->Setup(pack, workspaceSize, context); context->SetExecuteType(atb::EXECUTE_PRELAUNCH); operation->Execute(pack, (uint8_t*)workSpace, workspaceSize, context); aclmdlRIExecuteAsync(model, exeStream); } // Synchronize the stream and wait until the computation on the device is complete. ret = aclrtSynchronizeStream(exeStream); if (ret != 0) { std::cout << "sync error!" << std::endl; exit(0); } std::cout << "aclrtSynchronizeStream success!" << std::endl; // Print the values of the output tensors. PrintOutTensorValue(pack.outTensors.at(0)); } // Destroy the model running instance. aclmdlRIDestroy(model); } // Release the resources. atb::DestroyOperation(operation); atb::DestroyContext(context); for (size_t i = 0; i < pack.inTensors.size(); i++) { aclrtFree(pack.inTensors.at(i).deviceData); } for (size_t i = 0; i < pack.outTensors.size(); i++) { aclrtFree(pack.outTensors.at(i).deviceData); } aclrtFree(workSpace); aclrtFree(workSpace1); aclrtFree(workSpace2); aclrtDestroyStream(exeStream); aclrtResetDevice(deviceId); aclFinalize(); } |