本教程将介绍如何使用昇腾社区AscendCL应用开发接口进行运行时管理。并从AscendCL的初始化与去初始化、运行管理资源的申请与释放、数据传输、Stream管理、多Device切换以及同步等待等关键步骤来展开内容
以下教程涉及的所有代码都可以从样例介绍中获得
在使用AscendCL进行开发前,首先需要初始化AscendCL环境。这可以通过调用aclInit
接口来完成。如果默认配置已满足需求,可以直接传入NULL。
aclError ret = aclInit(NULL);
完成所有AscendCL调用后,需要调用aclFinalize
接口去初始化AscendCL。
ret = aclFinalize();
运行管理资源包括Device、Context和Stream。申请和释放这些资源的顺序很重要。
aclrtSetDevice
显式指定运算的Device。aclrtCreateContext
创建Context。aclrtCreateStream
创建Stream。示例代码:
// 初始化变量
int32_t deviceId=0 ;
aclrtContext context;
aclrtStream stream;
aclError ret = aclrtSetDevice(deviceId);
ret = aclrtCreateContext(&context, deviceId);
ret = aclrtCreateStream(&stream);
aclrtDestroyStream
销毁Stream。aclrtDestroyContext
销毁Context。aclrtResetDevice
重置Device。示例代码:
ret = aclrtDestroyStream(stream);
ret = aclrtDestroyContext(context);
ret = aclrtResetDevice(deviceId);
获取当前昇腾AI软件栈的运行模式,根据不同的运行模式,后续的接口调用方式不同
aclrtRunMode runMode;
extern bool g_isDevice;
ret = aclrtGetRunMode(&runMode);
g_isDevice = (runMode == ACL_DEVICE);
数据传输包含申请内存,将数据读入内存,内存复制三个环节
new
或malloc
,或者使用AscendCL提供的aclrtMallocHost
接口。aclrtMalloc
接口。aclrtMemcpy
接口aclrtMemcpyAsync
接口,并配合aclrtSynchronizeStream
接口实现同步等待。当前仅支持调用aclrtMemcpy接口执行同步Host内的内存复制任务,不支持调用aclrtMemcpyAsync接口执行异步Host内的内存复制功能
// 申请内存
uint64_t size = 1 * 1024 * 1024;
void* hostPtrA = NULL;
void* hostPtrB = NULL;
aclrtMallocHost(&hostPtrA, size);
aclrtMallocHost(&hostPtrB, size);
// 向内存中读入数据
ReadFile(fileName, hostPtrA, size);
// 同步内存复制
aclrtMemcpy(hostPtrB, size, hostPtrA, size, ACL_MEMCPY_HOST_TO_HOST);
// 释放资源
aclrtFreeHost(hostPtrA);
aclrtFreeHost(hostPtrB);
既支持同步内存复制,又支持异步内存复制
// 申请内存
uint64_t size = 1 * 1024 * 1024;
void* hostPtrA = NULL;
void* devPtrB = NULL;
aclrtMallocHost(&hostPtrA, size);
aclrtMalloc(&devPtrB, size, ACL_MEM_MALLOC_NORMAL_ONLY);
// 向内存中读入数据
ReadFile(fileName, hostPtrA, size);
// 同步内存复制
aclrtMemcpy(devPtrB, size, hostPtrA, size, ACL_MEMCPY_HOST_TO_DEVICE);
// 释放资源
aclrtFreeHost(hostPtrA);
aclrtFree(devPtrB);
// 申请内存
uint64_t size = 1 * 1024 * 1024;
void* hostAddr = NULL;
void* devAddr = NULL;
aclrtMallocHost(&hostAddr, size + 64);
aclrtMalloc(&devAddr, size, ACL_MEM_MALLOC_NORMAL_ONLY);
aclrtStream stream = NULL;
aclrtCreateStream(&stream);
// 获取到64字节对齐的地址
char *hostAlignAddr =(char *)hostAddr + 64 - ((uintptr_t)hostAddr % 64);
// 向内存中读入数据
ReadFile(fileName, hostAlignAddr, size);
// 异步内存复制
aclrtMemcpyAsync(devAddr, size, hostAlignAddr, size, ACL_MEMCPY_HOST_TO_DEVICE, stream);
aclrtSynchronizeStream(stream);
// 释放资源
aclrtDestroyStream(stream);
aclrtFreeHost(hostAddr);
aclrtFree(devAddr);
既支持同步内存复制,又支持异步内存复制
// 申请内存
uint64_t size = 1 * 1024 * 1024;
void* devPtrA = NULL;
void* hostPtrB = NULL;
aclrtMalloc(&devPtrA, size, ACL_MEM_MALLOC_NORMAL_ONLY);
aclrtMallocHost(&hostPtrB, size);
// 向内存中读入数据
ReadFile(fileName, devPtrA, size);
// 同步内存复制
aclrtMemcpy(hostPtrB, size, devPtrA, size, ACL_MEMCPY_DEVICE_TO_HOST);
// 释放资源
aclrtFree(devPtrA);
aclrtFreeHost(hostPtrB);
// 申请内存
uint64_t size = 1 * 1024 * 1024;
void* hostAddr = NULL;
void* devAddr = NULL;
aclrtMallocHost(&hostAddr, size + 64);
aclrtMalloc(&devAddr, size, ACL_MEM_MALLOC_NORMAL_ONLY);
aclrtStream stream = NULL;
aclrtCreateStream(&stream);
// 向内存中读入数据
ReadFile(fileName, devAddr, size);
// 获取到64字节对齐的地址
char *hostAlignAddr =(char *)hostAddr + 64 - ((uintptr_t)hostAddr % 64);
// 异步内存复制
aclrtMemcpyAsync(hostAlignAddr, size, devAddr, size, ACL_MEMCPY_DEVICE_TO_HOST, stream);
aclrtSynchronizeStream(stream);
// 释放资源
aclrtDestroyStream(stream);
aclrtFreeHost(hostAddr);
aclrtFree(devAddr);
// 申请内存
uint64_t size = 1 * 1024 * 1024;
void* devPtrA = NULL;
void* devPtrB = NULL;
aclrtMalloc(&devPtrA, size, ACL_MEM_MALLOC_NORMAL_ONLY);
aclrtMalloc(&devPtrB, size, ACL_MEM_MALLOC_NORMAL_ONLY);
// 向内存中读入数据
ReadFile(fileName, devPtrA, size);
// 同步内存复制
aclrtMemcpy(devPtrB, size, devPtrA, size, ACL_MEMCPY_DEVICE_TO_DEVICE);
// 异步内存复制
aclrtStream stream;
aclrtCreateStream(&stream);
aclrtMemcpyAsync(devPtrB, size, devPtrA, size, ACL_MEMCPY_DEVICE_TO_DEVICE, stream);
aclrtSynchronizeStream(stream);
// 释放资源
aclrtFree(devPtrA);
aclrtFree(devPtrB);
// AscendCL初始化
auto ret = aclInit(NULL);
// 查询Device 0和Device 1之间是否支持内存复制
int32_t canAccessPeer = 0;
ret = aclrtDeviceCanAccessPeer(&canAccessPeer, 0, 1);
// 1表示支持内存复制
if (canAccessPeer == 1) {
// Device 0下的操作,包括内存申请、数据写入、内存复制等
ret = aclrtSetDevice(0);
void *dev0;
ret = aclrtMalloc(&dev0, 10, ACL_MEM_MALLOC_HUGE_FIRST_P2P);
ret = aclrtMemset(dev0, 10, 1, 10);
// Device 1下的操作
ret = aclrtSetDevice(1);
ret = aclrtDeviceEnablePeerAccess(0, 0);
void *dev1;
ret = aclrtMalloc(&dev1, 10, ACL_MEM_MALLOC_HUGE_FIRST_P2P);
ret = aclrtMemset(dev1, 10, 0, 10);
// 执行复制,将Device 0上的内存数据复制到Device 1上
ret = aclrtMemcpy(dev1, 10, dev0, 10, ACL_MEMCPY_DEVICE_TO_DEVICE);
ret = aclrtResetDevice(1);
ret = aclrtSetDevice(0);
ret = aclrtResetDevice(0);
printf("P2P copy success\n");
} else {
printf("current device doesn't support p2p feature\n");
}
// AscendCL去初始化
aclFinalize();
注意事项
在AscendCL应用开发中,Stream是任务队列的抽象,用于管理任务的并行执行。理解并有效管理Stream对于提升程序性能和资源利用率至关重要。AscendCL提供了以下几种Stream管理机制:
在单线程环境下,可以创建一个Stream来管理任务的执行。
#include "acl/acl.h"
// 显式创建一个Stream
aclrtStream stream;
aclrtCreateStream(&stream);
// 调用触发任务的接口,传入stream参数
aclrtMemcpyAsync(dstPtr, dstSize, srcPtr, srcSize, ACL_MEMCPY_HOST_TO_DEVICE, stream);
// 调用aclrtSynchronizeStream接口,阻塞应用程序运行,直到指定Stream中的所有任务都完成。
aclrtSynchronizeStream(stream);
// Stream使用结束后,显式销毁Stream
aclrtDestroyStream(stream);
在单线程环境下,可以创建多个Stream来并行执行不同的任务。
#include "acl/acl.h"
int32_t deviceId = 0;
uint32_t modelId1 = 0;
uint32_t modelId2 = 1;
// 显式创建一个Stream
aclrtContext context;
aclrtStream stream1, stream2;
// 创建Context
aclrtCreateContext(&context, deviceId);
// 创建stream1
aclrtCreateStream(&stream1);
// 调用触发任务的接口,例如异步模型推理,任务下发在stream1
aclmdlDataset *input1, *output1;
aclmdlExecuteAsync(modelId1, input1, output1, stream1);
// 创建stream2
aclrtCreateStream(&stream2);
// 调用触发任务的接口,例如异步模型推理,任务下发在stream2
aclmdlDataset *input2, *output2;
aclmdlExecuteAsync(modelId2, input2, output2, stream2);
// 流同步
aclrtSynchronizeStream(stream1);
aclrtSynchronizeStream(stream2);
// 释放资源
aclrtDestroyStream(stream1);
aclrtDestroyStream(stream2);
aclrtDestroyContext(context);
在多线程环境下,每个线程可以创建自己的Stream来执行任务。
#include "acl/acl.h"
void runThread(aclrtStream stream) {
int32_t deviceId = 0;
aclrtContext context;
// 创建Context
aclrtCreateContext(&context, deviceId);
// 显式创建一个Stream
aclrtStream threadStream;
aclrtCreateStream(&threadStream);
// 释放资源
aclrtDestroyStream(threadStream);
aclrtDestroyContext(context);
}
// 创建2个线程,每个线程对应一个Stream
aclrtStream stream1, stream2;
std::thread t1(runThread, stream1);
std::thread t2(runThread, stream2);
// 显式调用join函数确保结束线程
t1.join();
t2.join();
注意事项
aclrtSynchronizeStream
接口来同步任务的执行。在AscendCL应用开发中,多Device切换是一个重要的特性,它允许开发者在多个昇腾AI处理器(Device)之间高效地切换和管理任务。
下图为:同步等待流程_多Device场景
aclrtSetCurrentContext:用于切换当前线程的Context,比使用aclrtSetDevice
接口效率更高。
aclrtSynchronizeDevice:用于等待特定Device上的所有计算任务结束。
初始化AscendCL:在使用AscendCL进行任何操作之前,需要先初始化AscendCL。
aclError ret = aclInit(NULL);
if (ret != ACL_ERROR_NONE) {
// 错误处理
}
创建Context:在多Device环境中,每个Device都有一个Context。需要为每个Device创建一个Context。
aclrtContext context[DEVICE_NUM];
for (int i = 0; i < DEVICE_NUM; ++i) {
ret = aclrtCreateContext(&context[i], i);
if (ret != ACL_ERROR_NONE) {
// 错误处理
}
}
切换Context和Device:在执行任务时,使用aclrtSetCurrentContext
接口切换到相应的Context,从而在对应的Device上执行任务。
// 假设我们要在Device 1上执行任务
aclrtSetCurrentContext(context[1]);
// 执行任务...
等待Device任务完成:在需要等待特定Device上的任务完成时,使用aclrtSynchronizeDevice
接口。
// 等待Device 2上的任务完成
aclrtSynchronizeDevice(2);
执行任务:在每个Device上执行相应的任务,如模型推理或算子执行。
// 模型推理示例
aclmdlExecuteAsync(modelId, input, output, stream);
释放资源:在任务完成后,释放Context和销毁所有资源。
for (int i = 0; i < DEVICE_NUM; ++i) {
aclrtDestroyContext(context[i]);
}
aclFinalize();
注意事项
aclrtSetCurrentContext
接口切换Context时,确保当前线程没有其他Device的任务在执行。aclFinalize
进行AscendCL的清理。在AscendCL应用开发中,同步等待是一个关键的概念,它确保了在异步计算场景下任务的正确执行顺序和资源的正确管理。
AscendCL提供了以下同步机制:
aclrtSynchronizeEvent
接口,阻塞应用程序运行,等待Event完成。aclrtSynchronizeStream
接口,阻塞应用程序运行,直到指定Stream中的所有任务都完成。aclrtStreamWaitEvent
接口,阻塞指定Stream的运行,直到指定的Event完成。aclrtSynchronizeDevice
接口,阻塞应用程序运行,直到正在运算中的Device完成运算。#include "acl/acl.h"
// 创建一个Event
aclrtEvent event;
aclrtCreateEvent(&event);
// 显式创建一个Stream
aclrtStream stream;
aclrtCreateStream(&stream);
// 在stream末尾添加了一个event
aclrtRecordEvent(event, stream);
// 阻塞应用程序运行,等待event发生,也就是stream执行完成
aclrtSynchronizeEvent(event);
// 显式销毁资源
aclrtDestroyStream(stream);
aclrtDestroyEvent(event);
#include "acl/acl.h"
// 显式创建一个Stream
aclrtStream stream;
aclrtCreateStream(&stream);
// 调用触发任务的接口,传入stream参数
aclrtMemcpyAsync(dstPtr, dstSize, srcPtr, srcSize, ACL_MEMCPY_HOST_TO_DEVICE, stream);
// 调用aclrtSynchronizeStream接口,阻塞应用程序运行,直到指定Stream中的所有任务都完成
aclrtSynchronizeStream(stream);
// Stream使用结束后,显式销毁Stream
aclrtDestroyStream(stream);
#include "acl/acl.h"
// 创建一个Event
aclrtEvent event;
aclrtCreateEvent(&event);
// 创建stream1
aclrtStream s1;
aclrtCreateStream(&s1);
// 创建stream2
aclrtStream s2;
aclrtCreateStream(&s2);
// 在s1末尾添加了一个event
aclrtRecordEvent(event, s1);
// 阻塞s2运行,直到指定event发生,也就是s1执行完成
aclrtStreamWaitEvent(s2, event);
// 显式销毁资源
aclrtDestroyStream(s2);
aclrtDestroyStream(s1);
aclrtDestroyEvent(event);
#include "acl/acl.h"
// 指定device
aclrtSetDevice(0);
// 创建context
aclrtContext ctx;
aclrtCreateContext(&ctx, 0);
// 创建stream
aclrtStream stream;
aclrtCreateStream(&stream);
// 阻塞应用程序运行,直到正在运算中的Device完成运算
aclrtSynchronizeDevice();
// 资源销毁
aclrtDestroyStream(stream);
aclrtDestroyContext(ctx);
aclrtResetDevice(0);
注意事项
aclrtStreamWaitEvent
接口来实现Stream间的同步等待。aclrtSynchronizeDevice
接口来等待特定Device上的任务完成。