aclnnBaddbmm&aclnnInplaceBaddbmm

Applicable Products

ProductSupported
Ascend 950PR/Ascend 950DT√
Atlas A3 training products/Atlas A3 inference products√
Atlas A2 training products/Atlas A2 inference products√
Atlas 200I/500 A2 inference products×
Atlas inference products√
Atlas training products√

Function

  • Description:

Computes the product of α and the matrix multiplication result of batch1 and batch2, and then adds the product of β and self. Note: batch1 and batch2 must be three-dimensional tensors. The two shapes support broadcast only in aclnnBaddbmm; broadcast between the two shapes in aclnnInplaceBaddbmm is blocked. self must support broadcast with the result of batch1@batch2. (Broadcast refers to expanding the smaller shape to match the larger shape so that the two shapes are compatible. Currently, only broadcast of (1, n) is supported, meaning that each corresponding dimension of the two tensors must be identical or one of them must be 1.)

  • Formula:

    out=βself+α(batch1@batch2)out = βself+α(batch1@batch2)

    Note: If β is 0, self is ignored and does not participate in the computation.

  • Example:

    The shape of self is [1, M, K], the shape of batch1@batch2 is [A, M, 1], and the shape of the output out is [A, M, K]. The numbers in each dimension must be identical or one of them must be 1. In this case, if the shape of self is [2, M, K], the broadcast condition is not met and an error is reported.

Function Prototype

  • aclnnBaddbmm and aclnnInplaceBaddbmm implement the same functionality. The differences are as follows. Select the appropriate operator based on your actual scenario.

    • aclnnBaddbmm: Requires creating a new output tensor object to store the computation result.

    • aclnnInplaceBaddbmm: Does not require creating a new output tensor object. The computation result is stored directly in the memory of the input tensor.

  • Each operator is divided into a two-phase API. You must first call the "aclnnBaddbmmGetWorkspaceSize" API to obtain the input parameters and calculate the required workspace size based on the process, and then call the "aclnnBaddbmm" API to perform the computation.

aclnnStatus aclnnBaddbmmGetWorkspaceSize(
  const aclTensor*    self,
  const aclTensor*    batch1,
  const aclTensor*    batch2,
  const aclScalar*    beta,
  const aclScalar*    alpha,
  aclTensor*          out,
  int8_t              cubeMathType,
  uint64_t*           workspaceSize,
  aclOpExecutor**     executor)
aclnnStatus aclnnBaddbmm(
  void*           workspace,
  uint64_t        workspaceSize,
  aclOpExecutor*  executor,
  aclrtStream     stream)
aclnnStatus aclnnInplaceBaddbmmGetWorkspaceSize(
  const aclTensor*    selfRef,
  const aclTensor*    batch1,
  const aclTensor*    batch2,
  const aclScalar*    beta,
  const aclScalar*    alpha,
  int8_t              cubeMathType,
  uint64_t*           workspaceSize,
  aclOpExecutor**     executor)
aclnnStatus aclnnInplaceBaddbmm(
  void*             workspace,
  uint64_t          workspaceSize,
  aclOpExecutor*    executor,
  aclrtStream       stream)

aclnnBaddbmmGetWorkspaceSize

  • Parameters:

    Parameter Input/Output Description Instruction Data Type Data Format Dimension (Shape) Non-contiguous Tensor
    self Input Input self in the formula.
    • The data type must be consistent with out.
    • The shape must satisfy the broadcast relationship with the shape of the last two dimensions of batch1@batch2. (Note: Only self can be broadcast to the same shape as the last two dimensions of batch1@batch2. The last two dimensions of batch1@batch2 cannot be broadcast to the same shape as self. For example, self: [2, 3, 5], batch1@batch2: [1, 1, 1] will cause an error.)
    • Supports empty tensors.
    BFLOAT16, FLOAT16, FLOAT32 ND 1-3 dimensions √
    batch1 Input Input batch1 in the formula.
    • The data type must satisfy the data type deduction rules with self and batch2 (see Deduction Relationship and Constraints).
    • The shape must satisfy the bmm input constraint relationship with batch2.
    • Empty tensors are supported.
    BFLOAT16, FLOAT16, FLOAT32 ND 3 √
    batch2 Input Input batch2 in the formula.
    • The data type must comply with the data type deduction rules with self and batch1 (see Deduction Relationship and Constraints).
    • The shape must satisfy the bmm input constraint relationship with batch1.
    • Supports empty tensors.
    BFLOAT16, FLOAT16, FLOAT32 ND 3 √
    beta Input Input β in the formula. The data type must be convertible to the data type derived from self and batch1@batch2 (see Deduction Relationship and Constraints). FLOAT, FLOAT16, DOUBLE, INT32, INT64, INT16, INT8, UINT8, BOOL - - -
    alpha Input The input α in the formula. The data type must be convertible to the data type derived from self and batch1@batch2 (see Deduction Relationship and Constraints). FLOAT, FLOAT16, DOUBLE, INT32, INT64, INT16, INT8, UINT8, BOOL - - -
    out Output Output out in the formula.
    • The data type must be consistent with self.
    • The shape must be consistent with the last two dimensions of batch1@batch2.
    • The format must be consistent with self and batch1@batch2.
    BFLOAT16, FLOAT16, FLOAT32 ND 3 √
    cubeMathType Input Specifies the computation logic of the Cube unit. If the input data types have a deduction relationship, this parameter processes the data type after deduction by default. Supported enumeration values are as follows:
    • 0: KEEP_DTYPE. Performs computation while retaining the input data type.
    • 1: ALLOW_FP32_DOWN_PRECISION. Allows down-precision computation of the input data.
    • 2: USE_FP16. Allows down-precision of the input to FLOAT16 for computation.
    • 3: USE_HF32. Allows down-precision of the input to HFLOAT32 for computation.
    • 4: USE_FP32_ADD. Uses a high-precision method for computation.
    INT8 - - -
    workspaceSize Output Returns the workspace size to be requested on the Device side. - - - - -
    executor Output parameter. Returns the operator executor, which contains the operator computation process. - - - - -
    • Atlas training products, Atlas inference products:

      • BFLOAT16 data type is not supported.

      • When the input data type is FLOAT32, cubeMathType=0 is not supported.

      • For cubeMathType=1, when the input data type is FLOAT32, it is converted to FLOAT16 for computation. When the input is of other data types, no processing is performed.

      • cubeMathType=3 and cubeMathType=4 are not supported.

    • Atlas A2 training products/Atlas A2 inference products, Atlas A3 training products/Atlas A3 inference products:

      • cubeMathType=0: When the input data type is FLOAT16 or BFLOAT16, the matrix multiplication uses FLOAT16/BFLOAT16 input and FLOAT32 output for transmission. When the input is of other data types, no processing is performed.

      • cubeMathType=1: When the input data type is FLOAT32, it is converted to HFLOAT32 for computation. When the input is of other data types, no processing is performed.

      • cubeMathType=2: When the input data type is FLOAT32, it is converted to FLOAT16 for computation. When the input is of other data types, no processing is performed.

      • cubeMathType=3: When the input data type is FLOAT32, it is converted to HFLOAT32 for computation. When the input is of other data types, no processing is performed.

      • cubeMathType=4: when the input data type is FLOAT16/BFLOAT16, the addmm process uses high-precision computation. In this case, broadcast between the input self and the matmul result matrix is not supported.

    • Ascend 950PR/Ascend 950DT:

      • cubeMathType=1: when the input data type is FLOAT32, it is converted to HFLOAT32 for computation; when the input is of other data types, no processing is performed.

      • cubeMathType=2: when the input data type is FLOAT32, it is converted to FLOAT16 for computation; when the input is of other data types, no processing is performed.

      • cubeMathType=3: when the input data type is FLOAT32, it is converted to HFLOAT32 for computation; when the input is of other data types, no processing is performed.

      • No processing is performed when cubeMathType is set to 4.

  • Return value

    aclnnStatus: return code. For details, see aclnn Return Code.

    The first-phase API performs input parameter validation and returns an error in the following scenarios:

    Return Value Error Code Description
    ACLNN_ERR_PARAM_NULLPTR 161001 The passed self, batch1, or batch2 is a null pointer.
    ACLNN_ERR_PARAM_INVALID 161002 The data type of self, batch1, batch2, or out is not within the supported range.
    The data format of self, batch1, batch2, or out is not within the supported range.
    The data types of self, batch1, batch2, or out are inconsistent.
    The data formats of self, batch1, batch2, or out are inconsistent.
    self cannot be broadcast with batch1@batch2.
    self and batch1@batch2 must both be empty tensors or both be non-empty tensors.
    The data type of self, batch1, batch2, or out does not satisfy the deduction rules.

aclnnBaddbmm

  • Parameters:

    Parameter Input/Output Description
    workspace Input Memory address of the workspace applied for on the Device side.
    workspaceSize Input Size of the workspace applied for on the Device side, obtained by the first-phase API aclnnBaddbmmGetWorkspaceSize.
    executor Input Operator executor that contains the operator computation flow.
    stream Input Specifies the stream for executing tasks.
  • Return value

    aclnnStatus: return code. For details, see aclnn Return Code.

aclnnInplaceBaddbmmGetWorkspaceSize

  • Parameters:

    Parameter Input/Output Description Instruction Data Type Data Format Shape Non-contiguous Tensor
    selfRef Input|Output Input/output tensor, i.e., the input self and out in the formula.
    • The data type and the data type of batch1@batch2 must satisfy the data type deduction rules (see Deduction Relationship and Constraints).
    • The last two dimensions of the shape must be consistent with the last two dimensions of batch1@batch2.
    • Supports empty tensors.
    BFLOAT16, FLOAT16, FLOAT32 ND 2~3 √
    batch1 Input. The input batch1 in the formula.
    • The data type must satisfy the data type deduction rules with the data types of self and batch2 (see Deduction Relationship and Constraints).
    • The shape must satisfy the bmm input constraint relationship with batch2.
    • Empty tensors are supported.
    BFLOAT16, FLOAT16, FLOAT32 ND 3 √
    batch2 Input Input batch2 in the formula.
    • The data type of batch2 and the data types of self and batch1 must satisfy the data type deduction rules (see Deduction Relationship and Constraints).
    • The shape must satisfy the bmm input constraint relationship with batch1.
    • Empty tensors are supported.
    BFLOAT16, FLOAT16, FLOAT32 ND 3 √
    beta Input. Input β in the formula. The data type must be convertible to the data type derived from self and batch1@batch2 (see Deduction Relationship and Constraints). FLOAT, FLOAT16, DOUBLE, INT32, INT64, INT16, INT8, UINT8, BOOL - - -
    alpha Input Input α in the formula. The data type must be convertible to the data type derived from self and batch1@batch2 (see Deduction Relationship and Constraints). FLOAT, FLOAT16, DOUBLE, INT32, INT64, INT16, INT8, UINT8, BOOL - - -
    cubeMathType Input Specifies the computation logic of the Cube unit. If the input data types have a deduction relationship, this parameter defaults to processing the data type after deduction. Supported enumeration values are as follows:
    • 0: KEEP_DTYPE, keeps the input data type for computation.
    • 1: ALLOW_FP32_DOWN_PRECISION, allows down-precision computation of the input data.
    • 2: USE_FP16, allows down-precision of the input to FLOAT16 for computation.
    • 3: USE_HF32, allows down-precision of the input to HFLOAT32 for computation.
    • 4: USE_FP32_ADD, uses a high-precision method for computation.
    INT8 - - -
    workspaceSize Output Returns the workspace size to be allocated on the Device side. - - - - -
    executor Output Returns the operator executor, which contains the operator computation flow. - - - - -
    • Atlas training products, Atlas inference products:

      • BFLOAT16 data type is not supported.

      • When the input data type is FLOAT32, cubeMathType=0 is not supported.

      • When cubeMathType=1 and the input data type is FLOAT32, the computation is converted to FLOAT16. No processing is performed for other input data types.

      • cubeMathType=3 and 4 are not supported.

    • Atlas training products and Atlas inference products:

      • BFLOAT16 data type is not supported.

      • cubeMathType=0 is not supported when the input data type is FLOAT32.

      • When cubeMathType=1 and the input data type is FLOAT32, it is converted to FLOAT16 for computation. No processing is performed for other data types.

      • cubeMathType=3 is not supported.

      • No processing is performed when cubeMathType=4.

    • Atlas A2 training products/Atlas A2 inference products, Atlas A3 training products/Atlas A3 inference products:

      • When cubeMathType=1 and the input data type is FLOAT32, it is converted to HFLOAT32 for computation. No processing is performed for other data types.

      • cubeMathType=2: When the input data type is FLOAT32, it is converted to FLOAT16 for computation; when the input is of other data types, no processing is performed.

      • cubeMathType=3: When the input data type is FLOAT32, it is converted to HFLOAT32 for computation; when the input is of other data types, no processing is performed.

      • cubeMathType=4: When the input data type is FLOAT16 or BFLOAT16, the addmm process uses high-precision computation. In this case, broadcast between the input self and the matmul result matrix is not supported.

    • Ascend 950PR/Ascend 950DT:

      • cubeMathType=1: When the input data type is FLOAT32, it is converted to HFLOAT32 for computation; when the input is of other data types, no processing is performed.

      • cubeMathType=2: when the input data type is FLOAT32, it is converted to FLOAT16 for computation; when the input is of other data types, no processing is performed.

      • cubeMathType=3: when the input data type is FLOAT32, it is converted to HFLOAT32 for computation; when the input is of other data types, no processing is performed.

      • cubeMathType=4: no processing is performed.

  • Return value

    aclnnStatus: return code. For details, see aclnn Return Code.

    The first-phase API performs input parameter validation and returns an error in the following scenarios:

    Return Value Error Code Description
    ACLNN_ERR_PARAM_NULLPTR 161001 The passed selfRef, batch1, or batch2 is a null pointer.
    ACLNN_ERR_PARAM_INVALID 161002 The data type of selfRef, batch1, or batch2 is not within the supported range.
    The data format of selfRef, batch1, or batch2 is not within the supported range.
    The data formats of selfRef, batch1, and batch2 are inconsistent.
    selfRef cannot be broadcast with batch1@batch2.
    The first dimensions of batch1 and batch2 are not equal.
    The dimensions of batch1 and batch2 are not three-dimensional.
    The last dimension of batch1 and the second-to-last dimension of batch2 are not equal.

aclnnInplaceBaddbmm

  • Parameters:

    Parameter Input/Output Description
    workspace Input. Specifies the workspace memory address applied for on the Device.
    workspaceSize Input. Specifies the workspace size applied for on the Device, obtained by the first-phase API aclnnInplaceBaddbmmGetWorkspaceSize.
    executor Input Operator executor, which contains the operator computation flow.
    stream Input Specifies the stream for task execution.
  • Return value

    aclnnStatus: return code. For details, see aclnn Return Code.

Constraints

  • Deterministic computation description:

aclnnBaddbmm&aclnnInplaceBaddbmm default to a deterministic implementation.

  • Atlas training products, Atlas inference products: The Cube unit does not support FLOAT32 computation. When the input is FLOAT32, set cubeMathType=1 (ALLOW_FP32_DOWN_PRECISION) to allow the API to internally cast to FLOAT16 for computation.

  • Atlas A2 training products/Atlas A2 inference products, Atlas A3 training products/Atlas A3 inference products: Data type deduction is not supported when one of the two inputs, batch1 and batch2, is BFLOAT16 and the other is FLOAT or FLOAT16.

Example

The following provides example code for reference only. For details about compilation and running, see Compile and Run Samples.

#include <iostream>
#include <vector>
#include "acl/acl.h"
#include "aclnnop/aclnn_baddbmm.h"

#define CHECK_RET(cond, return_expr) \
  do {                               \
    if (!(cond)) {                   \
      return_expr;                   \
    }                                \
  } while (0)

#define LOG_PRINT(message, ...)     \
  do {                              \
    printf(message, ##__VA_ARGS__); \
  } while (0)

int64_t GetShapeSize(const std::vector<int64_t>& shape) {
  int64_t shapeSize = 1;
  for (auto i : shape) {
    shapeSize *= i;
  }
  return shapeSize;
}

int Init(int32_t deviceId, aclrtStream* stream) {
  // Boilerplate: resource initialization.
  auto ret = aclInit(nullptr);
  CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclInit failed. ERROR: %d\n", ret); return ret);
  ret = aclrtSetDevice(deviceId);
  CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSetDevice failed. ERROR: %d\n", ret); return ret);
  ret = aclrtCreateStream(stream);
  CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateStream failed. ERROR: %d\n", ret); return ret);
  return 0;
}

template <typename T>
int CreateAclTensor(const std::vector<T>& hostData, const std::vector<int64_t>& shape, void** deviceAddr,
                    aclDataType dataType, aclTensor** tensor) {
  auto size = GetShapeSize(shape) * sizeof(T);
  // Call aclrtMalloc to allocate memory on the device side.
  auto ret = aclrtMalloc(deviceAddr, size, ACL_MEM_MALLOC_HUGE_FIRST);
  CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMalloc failed. ERROR: %d\n", ret); return ret);

  // Call aclrtMemcpy to copy data from the host side to the device memory.
  ret = aclrtMemcpy(*deviceAddr, size, hostData.data(), size, ACL_MEMCPY_HOST_TO_DEVICE);
  CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMemcpy failed. ERROR: %d\n", ret); return ret);

  // Compute the strides of a contiguous tensor.
  std::vector<int64_t> strides(shape.size(), 1);
  for (int64_t i = shape.size() - 2; i >= 0; i--) {
    strides[i] = shape[i + 1] * strides[i + 1];
  }

  // Call the aclCreateTensor API to create an aclTensor.
  *tensor = aclCreateTensor(shape.data(), shape.size(), dataType, strides.data(), 0, aclFormat::ACL_FORMAT_ND,
                            shape.data(), shape.size(), *deviceAddr);
  return 0;
}

int main() {
  // 1. (Boilerplate) Initialize the device/stream. Refer to the ACL API manual.
  // Fill in the deviceId based on your actual device.
  int32_t deviceId = 0;
  aclrtStream stream;
  auto ret = Init(deviceId, &stream);
  // Handle the check based on your needs.
  CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret);

  // 2. Construct the inputs and outputs. Customize the construction based on the API interface.
  std::vector<int64_t> selfShape = {1, 2, 4};
  std::vector<int64_t> batch1Shape = {1, 2, 3};
  std::vector<int64_t> batch2Shape = {1, 3, 4};
  std::vector<int64_t> outShape = {1, 2, 4};
  void* selfDeviceAddr = nullptr;
  void* batch1DeviceAddr = nullptr;
  void* batch2DeviceAddr = nullptr;
  void* outDeviceAddr = nullptr;
  aclTensor* self = nullptr;
  aclTensor* batch1 = nullptr;
  aclTensor* batch2 = nullptr;
  aclScalar* alpha = nullptr;
  aclScalar* beta = nullptr;
  aclTensor* out = nullptr;
  std::vector<float> selfHostData = {0, 1, 2, 3, 4, 5, 6, 7};
  std::vector<float> batch1HostData = {1, 1, 1, 2, 2, 2};
  std::vector<float> batch2HostData = {1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4};
  std::vector<float> outHostData(8, 0);
  int8_t cubeMathType = 1;
  float alphaValue = 1.2f;
  float betaValue = 1.0f;
  // Create the self aclTensor.
  ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_FLOAT, &self);
  CHECK_RET(ret == ACL_SUCCESS, return ret);
  // Create batch1 aclTensor.
  ret = CreateAclTensor(batch1HostData, batch1Shape, &batch1DeviceAddr, aclDataType::ACL_FLOAT, &batch1);
  CHECK_RET(ret == ACL_SUCCESS, return ret);
  // Create batch2 aclTensor.
  ret = CreateAclTensor(batch2HostData, batch2Shape, &batch2DeviceAddr, aclDataType::ACL_FLOAT, &batch2);
  CHECK_RET(ret == ACL_SUCCESS, return ret);
  // Create alpha aclScalar.
  alpha = aclCreateScalar(&alphaValue,aclDataType::ACL_FLOAT);
  CHECK_RET(alpha != nullptr, return ret);
  // Create beta aclScalar.
  beta = aclCreateScalar(&betaValue,aclDataType::ACL_FLOAT);
  CHECK_RET(beta != nullptr, return ret);
  // Create out aclTensor.
  ret = CreateAclTensor(outHostData, outShape, &outDeviceAddr, aclDataType::ACL_FLOAT, &out);
  CHECK_RET(ret == ACL_SUCCESS, return ret);

  uint64_t workspaceSize = 0;
  aclOpExecutor* executor = nullptr;

  // Example of calling the aclnnBaddbmm API
  // 3. Call the CANN operator library API.
  // Call the first-phase API of aclnnBaddbmm.
  ret = aclnnBaddbmmGetWorkspaceSize(self, batch1, batch2, alpha, beta, out, cubeMathType, &workspaceSize, &executor);
  CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnBaddbmmGetWorkspaceSize failed. ERROR: %d\n", ret); return ret);
  // Apply for device memory based on the workspaceSize calculated by the first-phase API.
  void* workspaceAddr = nullptr;
  if (workspaceSize > 0) {
    ret = aclrtMalloc(&workspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST);
    CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("allocate workspace failed. ERROR: %d\n", ret); return ret);
  }
  // Call the second-phase API of aclnnBaddbmm.
  ret = aclnnBaddbmm(workspaceAddr, workspaceSize, executor, stream);
  CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnBaddbmm failed. ERROR: %d\n", ret); return ret);

  // 4. (Boilerplate) Synchronize and wait for task execution to complete.
  ret = aclrtSynchronizeStream(stream);
  CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSynchronizeStream failed. ERROR: %d\n", ret); return ret);

  // 5. Obtain the output value and copy the result from the Device side memory to the Host side. Modify this step based on the specific API definition.
  auto size = GetShapeSize(outShape);
  std::vector<float> resultData(size, 0);
  ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), outDeviceAddr,
                    size * sizeof(resultData[0]), ACL_MEMCPY_DEVICE_TO_HOST);
  CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("copy result from device to host failed. ERROR: %d\n", ret); return ret);
  for (int64_t i = 0; i < size; i++) {
    LOG_PRINT("result[%ld] is: %f\n", i, resultData[i]);
  }

  // Example of aclnnInplaceBaddbmm API call
  // 3. Call the CANN operator library API.
  LOG_PRINT("\ntest aclnnInplaceBaddbmm\n");
  // Call the first-phase API of aclnnInplaceBaddbmm.
  ret = aclnnInplaceBaddbmmGetWorkspaceSize(self, batch1, batch2, alpha, beta, cubeMathType, &workspaceSize, &executor);
  CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnInplaceBaddbmmGetWorkspaceSize failed. ERROR: %d\n", ret); return ret);
  // Apply for device memory based on the workspaceSize calculated by the first-phase API.
  void* inplaceWorkspaceAddr = nullptr;
  if (workspaceSize > 0) {
    ret = aclrtMalloc(&inplaceWorkspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST);
    CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("allocate workspace failed. ERROR: %d\n", ret); return ret);
  }
  // Call the second-phase API of aclnnInplaceBaddbmm.
  ret = aclnnInplaceBaddbmm(inplaceWorkspaceAddr, workspaceSize, executor, stream);
  CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnInplaceBaddbmm failed. ERROR: %d\n", ret); return ret);

  // 4. (Boilerplate) Synchronize and wait for task execution to complete.
  ret = aclrtSynchronizeStream(stream);
  CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSynchronizeStream failed. ERROR: %d\n", ret); return ret);

  // 5. Obtain the output value and copy the result from the Device memory to the Host memory. Modify based on the specific API definition.
  ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), selfDeviceAddr,
                    size * sizeof(resultData[0]), ACL_MEMCPY_DEVICE_TO_HOST);
  CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("copy result from device to host failed. ERROR: %d\n", ret); return ret);
  for (int64_t i = 0; i < size; i++) {
    LOG_PRINT("result[%ld] is: %f\n", i, resultData[i]);
  }

  // 6. Release aclTensor and aclScalar. Modify based on the specific API definition.
  aclDestroyTensor(self);
  aclDestroyTensor(batch1);
  aclDestroyTensor(batch2);
  aclDestroyScalar(alpha);
  aclDestroyScalar(beta);
  aclDestroyTensor(out);

  // 7. Release device resources. Modify based on the specific API definition.
  aclrtFree(selfDeviceAddr);
  aclrtFree(batch1DeviceAddr);
  aclrtFree(batch2DeviceAddr);
  aclrtFree(outDeviceAddr);
  if (workspaceSize > 0) {
    aclrtFree(workspaceAddr);
  }
  aclrtFree(inplaceWorkspaceAddr);
  aclrtDestroyStream(stream);
  aclrtResetDevice(deviceId);
  aclFinalize();
  return 0;
}