前言
用 PyTorch 推理很简单,但生产环境里经常需要更底层的控制------比如 C++ 服务、嵌入式设备、或者极致的性能优化。这时候就要用 Ascend CL(Compute Language)直接调用 NPU。
下面从零写一个完整的推理程序,包含模型加载、输入预处理、推理、输出后处理。
一、初始化 Ascend CL 环境
任何 Ascend CL 程序的第一步:初始化运行时。
cpp
#include <acl/acl.h>
#include <iostream>
int main() {
// 1. 初始化 ACL 运行时
aclError ret = aclInit(nullptr);
if (ret != ACL_SUCCESS) {
std::cerr << "aclInit failed: " << ret << std::endl;
return -1;
}
// 2. 设置使用的设备
int deviceId = 0;
ret = aclrtSetDevice(deviceId);
if (ret != ACL_SUCCESS) {
std::cerr << "aclrtSetDevice failed: " << ret << std::endl;
aclFinalize();
return -1;
}
// 3. 创建 Context 和 Stream
aclrtContext context;
aclrtStream stream;
ret = aclrtCreateContext(&context, deviceId);
ret = aclrtCreateStream(&stream);
std::cout << "Ascend CL initialized successfully" << std::endl;
// ... 推理代码 ...
// 4. 释放资源
aclrtDestroyStream(stream);
aclrtDestroyContext(context);
aclrtResetDevice(deviceId);
aclFinalize();
return 0;
}
编译命令:
bash
g++ -o inference inference.cpp \
-I${ASCEND_HOME}/acl/include \
-L${ASCEND_HOME}/acl/lib64 \
-lacl
二、加载 .om 模型
模型加载分三步:加载文件、获取模型描述、创建推理句柄。
cpp
// 加载模型文件
aclmdlDesc* modelDesc = nullptr;
aclmdlHandle modelHandle = nullptr;
int loadModelFromFile(const char* modelPath) {
// 1. 加载 .om 文件
uint32_t modelId = 0;
aclError ret = aclmdlLoadFromFile(modelPath, &modelId);
if (ret != ACL_SUCCESS) {
std::cerr << "Load model failed: " << ret << std::endl;
return -1;
}
// 2. 获取模型描述
modelDesc = aclmdlCreateDesc();
ret = aclmdlGetDesc(modelDesc, modelId);
if (ret != ACL_SUCCESS) {
std::cerr << "Get model desc failed: " << ret << std::endl;
aclmdlUnload(modelId);
return -1;
}
// 3. 打印模型输入输出信息
size_t numInputs = aclmdlGetNumInputs(modelDesc);
size_t numOutputs = aclmdlGetNumOutputs(modelDesc);
std::cout << "Model inputs: " << numInputs << ", outputs: " << numOutputs << std::endl;
for (size_t i = 0; i < numInputs; i++) {
aclmdlIODims dims;
aclmdlGetInputDims(modelDesc, i, &dims);
std::cout << "Input " << i << " shape: [";
for (size_t j = 0; j < dims.dimCount; j++) {
std::cout << dims.dims[j];
if (j < dims.dimCount - 1) std::cout << ", ";
}
std::cout << "]" << std::endl;
}
return modelId;
}
三、准备输入数据
输入数据需要从 CPU 拷贝到 NPU 的 Device 内存。
cpp
// 创建输入 Dataset
aclmdlDataset* createInputDataset(void* hostData, size_t dataSize) {
// 1. 创建 Dataset
aclmdlDataset* dataset = aclmdlCreateDataset();
// 2. 分配 Device 内存
void* deviceBuffer = nullptr;
aclError ret = aclrtMalloc(&deviceBuffer, dataSize, ACL_MEM_MALLOC_HUGE_FIRST);
if (ret != ACL_SUCCESS) {
std::cerr << "aclrtMalloc failed" << std::endl;
return nullptr;
}
// 3. 拷贝数据从 Host 到 Device
ret = aclrtMemcpy(deviceBuffer, dataSize, hostData, dataSize, ACL_MEMCPY_HOST_TO_DEVICE);
if (ret != ACL_SUCCESS) {
std::cerr << "aclrtMemcpy failed" << std::endl;
aclrtFree(deviceBuffer);
return nullptr;
}
// 4. 创建 DataBuffer 并添加到 Dataset
aclDataBuffer* dataBuffer = aclCreateDataBuffer(deviceBuffer, dataSize);
aclmdlAddDatasetBuffer(dataset, dataBuffer);
return dataset;
}
预处理示例:图像归一化
cpp
// 对图像数据做归一化:mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]
void preprocessImage(float* input, const uint8_t* image, int height, int width) {
const float mean[3] = {0.485f, 0.456f, 0.406f};
const float std[3] = {0.229f, 0.224f, 0.225f};
for (int c = 0; c < 3; c++) {
for (int h = 0; h < height; h++) {
for (int w = 0; w < width; w++) {
int idx = c * height * width + h * width + w;
int imgIdx = h * width * 3 + w * 3 + c;
input[idx] = (image[imgIdx] / 255.0f - mean[c]) / std[c];
}
}
}
}
四、执行推理
推理是异步的,需要用 Stream 同步。
cpp
// 执行推理
int executeInference(uint32_t modelId, aclmdlDataset* input, aclmdlDataset* output, aclrtStream stream) {
// 1. 执行推理(异步)
aclError ret = aclmdlExecuteAsync(modelId, input, output, stream);
if (ret != ACL_SUCCESS) {
std::cerr << "aclmdlExecuteAsync failed: " << ret << std::endl;
return -1;
}
// 2. 同步等待推理完成
ret = aclrtSynchronizeStream(stream);
if (ret != ACL_SUCCESS) {
std::cerr << "aclrtSynchronizeStream failed: " << ret << std::endl;
return -1;
}
std::cout << "Inference completed" << std::endl;
return 0;
}
五、获取输出结果
输出数据在 NPU 的 Device 内存,需要拷贝回 CPU 才能读取。
cpp
// 获取输出数据
int getOutputData(aclmdlDataset* outputDataset, float* hostOutput, size_t outputSize) {
// 1. 获取输出 DataBuffer
aclDataBuffer* dataBuffer = aclmdlGetDatasetBuffer(outputDataset, 0);
void* deviceBuffer = aclGetDataBufferAddr(dataBuffer);
size_t bufferSize = aclGetDataBufferSizeV2(dataBuffer);
// 2. 拷贝数据从 Device 到 Host
aclError ret = aclrtMemcpy(hostOutput, outputSize, deviceBuffer, bufferSize, ACL_MEMCPY_DEVICE_TO_HOST);
if (ret != ACL_SUCCESS) {
std::cerr << "aclrtMemcpy device to host failed" << std::endl;
return -1;
}
return 0;
}
后处理示例:Softmax + Top-K
cpp
#include <algorithm>
#include <vector>
void softmax(float* data, int size) {
float maxVal = *std::max_element(data, data + size);
float sum = 0.0f;
for (int i = 0; i < size; i++) {
data[i] = expf(data[i] - maxVal);
sum += data[i];
}
for (int i = 0; i < size; i++) {
data[i] /= sum;
}
}
std::vector<int> topK(float* probs, int size, int k) {
std::vector<std::pair<float, int>> pairs;
for (int i = 0; i < size; i++) {
pairs.push_back({probs[i], i});
}
std::partial_sort(pairs.begin(), pairs.begin() + k, pairs.end(),
[](auto& a, auto& b) { return a.first > b.first; });
std::vector<int> indices;
for (int i = 0; i < k; i++) {
indices.push_back(pairs[i].second);
}
return indices;
}
六、完整推理流程
cpp
int main() {
// 初始化
aclInit(nullptr);
aclrtSetDevice(0);
aclrtContext context;
aclrtStream stream;
aclrtCreateContext(&context, 0);
aclrtCreateStream(&stream);
// 加载模型
uint32_t modelId = loadModelFromFile("resnet50.om");
// 准备输入
float inputData[3 * 224 * 224];
uint8_t imageData[224 * 224 * 3];
preprocessImage(inputData, imageData, 224, 224);
aclmdlDataset* input = createInputDataset(inputData, sizeof(inputData));
// 准备输出
aclmdlDataset* output = aclmdlCreateDataset();
void* outputBuffer;
aclrtMalloc(&outputBuffer, 1000 * sizeof(float), ACL_MEM_MALLOC_HUGE_FIRST);
aclDataBuffer* outputData = aclCreateDataBuffer(outputBuffer, 1000 * sizeof(float));
aclmdlAddDatasetBuffer(output, outputData);
// 执行推理
executeInference(modelId, input, output, stream);
// 获取输出
float outputProb[1000];
getOutputData(output, outputProb, sizeof(outputProb));
// 后处理
softmax(outputProb, 1000);
auto top5 = topK(outputProb, 1000, 5);
std::cout << "Top 5 classes: ";
for (int idx : top5) {
std::cout << idx << " (" << outputProb[idx] << ") ";
}
std::cout << std::endl;
// 释放资源
aclmdlUnload(modelId);
aclrtDestroyStream(stream);
aclrtDestroyContext(context);
aclrtResetDevice(0);
aclFinalize();
return 0;
}
七、性能优化技巧
1. 批量推理
单次推理的开销包含模型加载、内存分配、Stream 同步等。批量推理可以分摊这些固定开销。
cpp
// batch_size = 8
float inputData[8 * 3 * 224 * 224];
aclmdlDataset* input = createInputDataset(inputData, sizeof(inputData));
2. 输入 Buffer 复用
多次推理时,Device 内存可以复用,不用每次分配。
cpp
// 提前分配,多次使用
void* inputBuffer = nullptr;
aclrtMalloc(&inputBuffer, inputSize, ACL_MEM_MALLOC_HUGE_FIRST);
// 第一次推理
aclrtMemcpy(inputBuffer, inputSize, data1, inputSize, ACL_MEMCPY_HOST_TO_DEVICE);
aclmdlExecuteAsync(modelId, input, output, stream);
// 第二次推理(复用同一块内存)
aclrtMemcpy(inputBuffer, inputSize, data2, inputSize, ACL_MEMCPY_HOST_TO_DEVICE);
aclmdlExecuteAsync(modelId, input, output, stream);
3. 异步流水线
预处理、推理、后处理用不同的 Stream 并行。
cpp
aclrtStream preprocessStream, inferenceStream, postprocessStream;
aclrtCreateStream(&preprocessStream);
aclrtCreateStream(&inferenceStream);
aclrtCreateStream(&postprocessStream);
// 流水线:第一批预处理、第二批推理、第三批后处理同时进行
参考资源
- Ascend CL API 文档:https://www.hiascend.com/document/detail/zh/CANN/
- C++ 推理样例:https://atomgit.com/cann/samples
- ACL 性能优化指南:https://www.hiascend.com/document/detail/zh/CANN/
- 昇腾推理最佳实践:https://www.hiascend.com/document/detail/zh/CANN/
总结
用 Ascend CL 写推理程序的核心流程:初始化运行时 → 加载 .om 模型 → 准备输入 Dataset → 执行推理 → 获取输出 Dataset。每个步骤都有对应的 API,理解了内存模型(Host vs Device)之后就不难。性能优化集中在三点:批量推理分摊固定开销、Device 内存复用减少分配次数、异步流水线让预处理/推理/后处理并行。生产环境里用 C++ 直接调用 ACL,比 PyTorch 更灵活,延迟也更可控。
