ATB Graph Offloading

Applicable Products

Hardware

Supported

Atlas 350 accelerator card

Atlas A3 inference products/Atlas A3 training products

Atlas A2 training products/Atlas A2 inference products

Atlas training products

x

Atlas inference products

x

Atlas 200I/500 A2 inference products

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).
      • aclmdlRICaptureEnd

        Call the aclmdlRICaptureEnd API to stop capturing the device-side tasks delivered on the stream and obtain the aclmdlRI model running instance.

    • Model execution

      Execute all device-side tasks saved in the aclmdlRI model running instance during the capture phase.

      • aclmdlRIExecuteAsync

        Call the aclmdlRIExecuteAsync API to execute all captured device-side tasks saved in the aclmdlRI model running instance.

    • 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.

  • 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.

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

Before compiling this case, set the ascend-toolkit and atb environment variables.
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();
}