DriveOS分析系列1-内存管理

项目类型版本号
DriveOS分析系列SummaryV0.1

Nvidia异构计算内存管理


声明:本文章从公开资料中分析,部分底层机制实现并不完全正确。


术语

Block: Stream的一个小模块,在进程内管理stream的行为。block以树状结构互联以组成任意Stream。

Consumer: 一个应用组件负责从Stream中获取待它处理的数据。

Element: 一个Packet中的单一Buffer。

Frame: 一个单帧图像。

Packet : 是一个Buffer的集合,包含Stream的Payload数据。Stream可以包含使用多个Packet。

Payload: Packet里具体的数据内容 和 与之相关的 Stream序列中某个序列号的同步原语。

Producer: 负责产生数据并将其插入流。

Stream: 一种机制,该机制将一系列的计算数据从一个应用组件传递到另一个应用组件。


先写总结:

  • 暴露出来的API较多,需要用户掌握较多概念后,严格按照框架要求编程。
  • 非常关注概念的阐述。
  • 有功能安全版本,同时有非功能安全版本。
  • 公开的文档中描述没有更底层的实现描述。
  • 能够较高效的实现跨进程,跨虚拟机OS,跨芯片的数据传输。
  • 初始化准备工作较多,对开发者要求较高。
  • 底层SMMU的数据通路打通依赖暴露给用户的系列API。(强约束)

TL;DR;

一: Nvidia 共享内存底层对象

EGLStream可以将标准的图像格式EGL数据流在OpenGL,CUDA,NvMedia之间流转。聚焦在Kronos API框架下2D+3D渲染图像的高性能传输。

NvSciStream 中 NvsciBuf 则定义了Nvidia之间各个加速器的共享内存传输能力。

换言之。NVidia的世界里,为了充分发挥CUDA的战斗力。

  • 当需要用到GPU的渲染能力时,使用EGLStream来和开放的图形图像标准来做交互。车载比较典型的场景是:摄像头吐出来的图像数据,通过EGLStream喂给应用程序先用OpenCL进行计算,在给GPU来渲染框架,渲染出图像。
  • 当需要用到GPU的通用算力时,使用NVStream来和CUDA之间做零拷贝的传输。车载场景下比较典型的场景是:车载Sensor吐出来的数据,直接喂给cuDNN做物体识别等推理。

那么,为什么有了EGLStream,还要创造NvStream呢?Nvidia 在 Drive OS的文档中给出了理由:

NVIDIA continues to support EGL, but EGL is not suitable for the rigorous requirements of a safety-certified system. Some reasons:

• Resources are allocated using one library. That library has no knowledge that resources will be shared with another library. There is therefore no guarantee that resources will be allocated in a way that meets the requirements of the other library. Mapping them may not be possible.

• EGL understands only two-dimensional image data. It cannot handle tensors or other non-image sensor data.

• The EGL interfaces were not designed with safety in mind and have failure modes not allowed in a safety-certified system.

总结为三个理由:

1, EGLStream没有全局的,系统的分配机制来满足所有应用的共享需求。

2, EGLStream的标准来源于图形图像领域。主要是二维的图像定义。

3, 功能安全设计不达标。

结论:NvSciStream 是为车载异构计算而生共享内存解决方案。

SOC内部承载应用非常丰富,NVSciStream怎么统一这些不同格式的数据呢?

习惯了底层软件编程的人,一般只会给用户一个buffer地址和长度。而Nvidia没有这么粗暴,只给数据指针和长度就像给用户一个抽象类,具体的格式需要用户自己转换。像极了Linux世界,你要运行APP的话,先丢给你一个源代码,自己去编译!

为了统一异构计算的内存共享问题,Nv给出两个底层管理类。 这两个类是构建后面高级类的基础:

  • NvSciBuf
  • NvSciBufModule
  • NvSciBufAttrList

DRIVE OS将缓存**NVSciBuf**进行分类,他们可以从SOC内部的 CVSRAM 或者 系统内存 中分配缓存。

缓存分类用途属性
RawBuffer通用存储空间,应用程序用来存储数据NvSciBufGeneralAttrKey_RawBuffer
Image存储图像数据,有自己的格式。方便编解码。NvSciBufGeneralAttrKey_Image
ImagePyramid一组图像,每个图像的长宽不一样,按照一定的顺序堆叠NvSciBufGeneralAttrKey_ImagePyramid
NvSciBufArray表示整形或者浮点等基础数据类型的数组NvSciBufGeneralAttrKey_NvSciBufArray
TensorNN模型的输入数据和输出数据,标准化的NN模型输入输出类NvSciBufGeneralAttrKey_Tensor

Nvidia的方案里给每个Buffer都设置了属性:

  1. NvSciBufGeneralAttrKey_Types 确定是以上哪种Buffer类型
  2. NvSciBufGeneralAttrKey_NeedCpuAccess CPU是否可以访问该Buffer类型。
  3. NvSciBufGeneralAttrKey_RequiredPerm 定义访问该Buffer时,授予给Buffer的权限。

NvSciBufModule

NvSciBufModule管理NVSciBuff,为各个逻辑加速器建立逻辑关系,是非常关键的一个类。为了创建一个NvSciBufModule,需要先定义好属性列表,有三类属性列表:

  • 非调和属性列表:不能通过该列表分配可以在加速器之间流转的NVSciBuf, 仅仅给Buffer来定义属性。相当于原始属性列表。
  • 调和属性列表:将非调和的属性调和成功后的属性列表。通过该列表可以分配在加速器之间流转的NVSciBuf。
  • 冲突属性列表: 如果调和各定义的属性列表失败,则返回冲突属性列表。无法创建NVSciBuf。

应用通过**NvSciBufAttrListReconcile**这个API,检查了Buffer属性的一致性,从而确定接下来 NVSciBuffer 在系统哪些加速器之间进行传输。

NvSciBufAttrList

定义Buffer的属性。

单进程内异构器件使用共享内存示例代码:

// 定义属性Attr的具体值
NvSciBufType bufType = NvSciBufType_RawBuffer;                          
uint64_t rawsize = (128 * 1024);  // Allocate 128K Raw-buffer                                        
uint64_t align = (4 * 1024);  //Buffer Alignment of 4K                                             
bool cpuaccess_flag = false;                                             
                                                                           
NvSciBufAttrKeyValuePair rawbuffattrs[] = {                              
     { NvSciBufGeneralAttrKey_Types, &bufType, sizeof(bufType) },        
     { NvSciBufRawBufferAttrKey_Size, &rawsize, sizeof(rawsize) },       
     { NvSciBufRawBufferAttrKey_Align, &align, sizeof(align) },          
     { NvSciBufGeneralAttrKey_NeedCpuAccess, &cpuaccess_flag,    /*该内存允许CPU访问*/        
            sizeof(cpuaccess_flag) },                                    
};  

NvSciError  err;
NvSciBufModule bufmodule;
err = NvSciBufModuleOpen(&bufmodule);
NvSciBufAttrList AttrList1;    
unreconciledList[0] = AttrList1;
// 将AttrList1 和 bufmodule 关联                                                                          
err = NvSciBufAttrListCreate(bufmodule, &AttrList1);                  
if (err != NvSciError_Success) {
   goto fail;
}                                                                                       
 
// 将属性值设置到具体的属性列表。
err = NvSciBufAttrListSetAttrs(AttrList1, rawbuffattrs,               
        sizeof(rawbuffattrs)/sizeof(NvSciBufAttrKeyValuePair)); 

NvSciBufAttrList unreconciledList[2] = {NULL};
NvSciBufAttrList reconciledList = NULL;
NvSciBufAttrList ConflictList = NULL;
  
// 假设已经创建了 AttrList2
unreconciledList[1] = AttrList2; 

// 调和属性列表,reconciledList 和 ConflictList 也关联到 bufmodule
err = NvSciBufAttrListReconcile(
        unreconciledList,        /* array of unreconciled lists */
        2,                       /* size of this array */
        &reconciledList,         /* output reconciled list */
        &ConflictList);         /* conflict description filled in case of reconciliation failure */
if (err != NvSciError_Success) {
    goto fail;
}
  
/* ... */
  
// 属性调和好后,允许申请NvSciBuf对象。
/* Allocate a Buffer using reconciled attribute list and the
* created NvSciBufObj will be associated with the module to
* which reconciledAttrlist belongs to.
*/
err = NvSciBufAttrListObjAlloc(reconciledAttrlist,
&nvscibufobj); 
if (err != NvSciError_Success) {
goto fail;
}

// 通过NvSciBufObj获取已调和属性列表 
/* Get the associated reconciled attrlist of the object. */
err = NvSciBufObjGetAttrList(nvscibufobj,
&objReconciledAttrList);
if (err != NvSciError_Success) {
goto fail;
}

// 通过NvSciBufObj获取CPU的指针数据。
err = NvSciBufObjGetCpuPtr(nvscibufobj, &va_ptr);
if (err != NvSciError_Success) {
goto fail;
}

// 扫尾工作 
NvSciBufAttrListFree(objReconciledAttrList);
NvSciBufObjFree(nvscibufobj);
NvSciSyncAttrListFree(AttrList1);
NvSciSyncAttrListFree(AttrList2);
NvSciSyncAttrListFree(reconciledList); // In case of successful reconciliation.
NvSciSyncAttrListFree(ConflictList); // In case of failed reconciliation.

如果进程间需要共享这些 NvSciBuf, 需要通过NvSciIpcOpenEndpoint打开一个IPC通道,进行进程间的属性调和。

NvSciIpc是跨进程共享内存的一个核心,可以完成 跨进程,跨虚拟机OS,跨芯片 的进程间通信。

NvSciIpcEndpoint ipcEndpoint = 0;
 
 
err = NvSciIpcInit();
if (err != NvSciError_Success) {
    goto fail;
}
err = NvSciIpcOpenEndpoint("ipc_endpoint", &ipcEndpoint);
if (err != NvSciError_Success) {
    goto fail;
}
 
 
/* ... */
 
NvSciIpcCloseEndpoint(ipcEndpoint);
NvSciIpcDeinit();

进程一:

NvSciBufAttrList AttrList1 = NULL;
void* ListDesc = NULL;
size_t ListDescSize = 0U;
 
// 通过ipcEndPoint关联,导出本进程的期望的AttrList1。API将这些属性导出到底层框架做调和。 
err = NvSciBufAttrListIpcExportUnreconciled(
    &AttrList1,               /* array of unreconciled lists to be exported */
    1,                             /* size of the array */
    ipcEndpoint,                   /* valid and opened NvSciIpcEndpoint intended to send the descriptor through */
    &ListDesc,               /* The descriptor buffer to be allocated and filled in */
    &ListDescSize );         /* size of the newly created buffer */
if (err != NvSciError_Success) {
    goto fail;
}
 
/* send the descriptor to the process2 */
//当前进程收到调和后的属性列表
/* wait for process 1 to reconcile and export reconciled list */
err = NvSciBufAttrListIpcImportReconciled(
    module,                    /* NvSciBuf module using which this attrlist to be imported */
    ipcEndpoint,                   /* valid and opened NvSciIpcEndpoint on which the descriptor is received */
    ListDesc,               /* The descriptor buffer to be imported */
    ListDescSize,          /* size of the descriptor buffer */
    &AttrList1,            /* array of unreconciled lists to be used for validating the reconciled list */
    1,                     /* Number or unreconciled lists */
    &reconciledAttrList,               /* Imported reconciled list */ <---重点
 if (err != NvSciError_Success) {
    goto fail;
}

进程二:

void* ListDesc = NULL;
size_t ListDescSize = 0U;
NvSciBufAttrList unreconciledList[2] = {NULL};
NvSciBufAttrList reconciledList = NULL;
NvSciBufAttrList newConflictList = NULL;
NvSciBufAttrList AttrList2 = NULL;
NvSciSyncAttrList importedUnreconciledAttrList = NULL;
 
// 等待进程一的属性调和请求。
/* create the local AttrList */
/* receive the descriptor from the other process */
err = NvSciBufAttrListIpcImportUnreconciled(module, ipcEndpoint,
    ListDesc, ListDescSize,
    &importedUnreconciledAttrList);
if (err != NvSciError_Success) {
    goto fail;
}
 
/* gather all the lists into an array and reconcile */
unreconciledList[0] = AttrList2;
unreconciledList[1] = importedUnreconciledAttrList;
 
//应用属性调和
err = NvSciBufAttrListReconcile(unreconciledList, 2, &reconciledList,
        &newConflictList);
if (err != NvSciError_Success) {
    goto fail;
}
 
err = NvSciBufAttrListIpcExportReconciled(
    &AttrList1,               /* array of unreconciled lists to be exported */
    ipcEndpoint,                   /* valid and opened NvSciIpcEndpoint intended to send the descriptor through */
    &ListDesc,               /* The descriptor buffer to be allocated and filled in */
    &ListDescSize );         /* size of the newly created buffer */
if (err != NvSciError_Success) {
    goto fail;
}

可以看出,进程间的属性调和是一个IPC过程。一方Export属性,另一方Import属性,然后在各自进程空间内进行属性一致性检查。

接下来开始进入Buffer对象的使用阶段。

进程一:通过ipcEndpoint进行关联,发送具体的NvSciBuf对象。

/* process1 */
void* objAndList;
size_t objAndListSize;
 
 
err = NvSciBufIpcExportAttrListAndObj(
    bufObj,                        /* bufObj to be exported (the reconciled list is inside it) */
    NvSciBufAccessPerm_ReadOnly,   /* permissions we want the receiver to have */
    ipcEndpoint,                    /* IpcEndpoint via which the object is to be exported */
    &objAndList,                    /* descriptor of the object and list to be communicated */
    &objAndListSize);               /* size of the descriptor */
 
 
 
 /* send via Ipc */

进程二:通过ipcEndpoint进行关联,接收具体的NvSciBuf对象。

/* process2 */
void* objAndList;
size_t objAndListSize;
 
 
err = NvSciBufIpcImportAttrListAndObj(
    module,                        /* NvSciBufModule use to create original unreconciled lists in the waiter */
    ipcEndpoint,                   /* ipcEndpoint from which the descriptor was received */
    objAndList,                    /* the desciptor of the buf obj and associated reconciled attribute list received from the signaler */
    objAndListSize,                /* size of the descriptor */
    &AttrList1,               /* the array of original unreconciled lists prepared in this process */
    1,                             /* size of the array */
    NvSciBufAccessPerm_ReadOnly,  /* permissions expected by this process */
    10000U,                        /* timeout in microseconds. Some primitives might require time to transport all needed resources */
    &bufObj);                     /* buf object generated from the descriptor */
 
 
/* use the buf object */
 
 
NvSciBufObjFree(bufObj);

至此,主干流程完成。

前面还提到几种数据类型:NvMediaImage,NvMediaTensor,NvMediaArray。 Nvidia通过提供一系列接口来和NvSciBuf映射。

1, NVImage和NvSciBuf转换

NvMediaDevice *device ;
NvMediaStatus status;
NvSciError  err;
NvSciBufModule module;
NvSciBufAttrList attrlist, conflictlist;
NvSciBufObj bufObj;
NvMediaImage *image;
NvMediaSurfaceType nvmsurfacetype;
NvMediaSurfAllocAttr surfAllocAttrs[8];
NVM_SURF_FMT_DEFINE_ATTR(surfFormatAttrs);
 
/*NvMedia related initialization */
device = NvMediaDeviceCreate();
 
status = NvMediaImageNvSciBufInit();
 
/*NvSciBuf related initialization*/  
err = NvSciBufModuleOpen(&module);
 
/*Create NvSciBuf attribute list*/
err = NvSciBufAttrListCreate(module, &attrlist);
 
/*Initialize surfFormatAttrs and surfAllocAttrs as required */
....
....
 
/* Get NvMediaSurfaceType */
nvmsurfacetype = NvMediaSurfaceFormatGetType(surfFormatAttrs, NVM_SURF_FMT_ATTR_MAX);
 
/*Ask NvMedia to fill NvSciBufAttrs corresponding to nvmsurfacetype and  surfAllocAttrs*/
status = NvMediaImageFillNvSciBufAttrs(device, nvmsurfacetype, surfAllocAttrs, numsurfallocattrs, 0, attrlist);
 
/*Reconcile the NvSciBufAttrs and then allocate a NvSciBufObj */
err = NvSciBufAttrListReconcileAndObjAlloc(&attrlist, 1, bufobj,  &conflictlist);

// 转换API
/*Create NvMediaImage from NvSciBufObj */
status = NvMediaImageCreateFromNvSciBuf(device, bufobj, &image);
 
/*Free the NvSciBufAttrList which is no longer required */
err = NvSciBufAttrListFree(attrlist);
 
// 使用通用的NvImage
/*Use the image as input or output for any of the Nvmedia */
....
....
 
/*Free the resources after use*/
 
/*Destroy NvMediaImage */
NvMediaImageDestroy(image);
 
/*NvMedia related Deinit*/
NvMediaImageNvSciBufDeinit();
NvMediaDeviceDestroy(device);
 
/*NvSciBuf related deinit*/
NvSciBufObjFree(bufobj);
NvSciBufModuleClose(module);

1, CUDA pointer 和NvSciBuf转换 , CUDA可以利用裸指针做输入,NN推理或训练场景使用。

/*********** Allocate NvSciBuf object ************/                                                                                                                                                                
    // Raw Buffer Attributes for CUDA
    NvSciBufType bufType = NvSciBufType_RawBuffer;
    uint64_t rawsize = SIZE;
    uint64_t align = 0;
    bool cpuaccess_flag = true;
    NvSciBufAttrValAccessPerm perm = NvSciBufAccessPerm_ReadWrite;
 
 
    uint64_t gpuId[] = {};
    cuDeviceGetUuid(&uuid, dev));
    gpuid[0] = (uint64_t)uuid.bytes;
     
    // Fill in values
    NvSciBufAttrKeyValuePair rawbuffattrs[] = {                              
         { NvSciBufGeneralAttrKey_Types, &bufType, sizeof(bufType) },        
         { NvSciBufRawBufferAttrKey_Size, &rawsize, sizeof(rawsize) },       
         { NvSciBufRawBufferAttrKey_Align, &align, sizeof(align) },          
         { NvSciBufGeneralAttrKey_NeedCpuAccess, &cpuaccess_flag,            
                sizeof(cpuaccess_flag) },
         { NvSciBufGeneralAttrKey_RequiredPerm, &perm, sizeof(perm) },
         { NvSciBufGeneralAttrKey_GpuId, &gpuid, sizeof(gpuId) },                                 
    };                                                                       
 
    // Create list by setting attributes
    err = NvSciBufAttrListSetAttrs(attrListBuffer, rawbuffattrs,               
            sizeof(rawbuffattrs)/sizeof(NvSciBufAttrKeyValuePair)); 
                    
    NvSciBufAttrListCreate(NvSciBufModule, &attrListBuffer);
 
    // Reconcile And Allocate
    NvSciBufAttrListReconcile(&attrListBuffer, 1, &attrListReconciledBuffer, &attrListConflictBuffer)
    NvSciBufObjAlloc(attrListReconciledBuffer, &bufferObjRaw);
 
 
    /*************** Query NvSciBuf Object **************/
     NvSciBufAttrKeyValuePair bufattrs[] = {
                {NvSciBufRawBufferAttrKey_Size, NULL, 0},
    };
    NvSciBufAttrListGetAttrs(retList, bufattrs, sizeof(bufattrs)/sizeof(NvSciBufAttrKeyValuePair)));
    ret_size = *(static_cast<const uint64_t*>(bufattrs[0].value));
 
 
    /*************** NvSciBuf Registration With CUDA **************/
    // Fill up CUDA_EXTERNAL_MEMORY_HANDLE_DESC
    cudaExternalMemoryHandleDesc memHandleDesc;
    memset(&memHandleDesc, 0, sizeof(memHandleDesc));
    memHandleDesc.type = cudaExternalMemoryHandleTypeNvSciBuf;
    memHandleDesc.handle.nvSciBufObject = bufferObjRaw; // 拿到buffer object
    memHandleDesc.size = ret_size;
    cudaImportExternalMemory(&extMemBuffer, &memHandleDesc);
 
 
    /************** Mapping to CUDA ******************************/
    cudaExternalMemoryBufferDesc bufferDesc;
    memset(&bufferDesc, 0, sizeof(bufferDesc));
    bufferDesc.offset = offset = 0;
    bufferDesc.size = ret_size;
    cudaExternalMemoryGetMappedBuffer(&dptr, extMemBuffer, &bufferDesc); // 拿到buffer Object里的裸指针。
 
 
    /************** CUDA Kernel ***********************************/
    // Run CUDA Kernel on dptr
 
 
    /*************** Free CUDA mappings *****************************/
    cudaFree();
    cudaDestroyExternalMemory(extMemBuffer);
 
    /***************** Free NvSciBuf **********************************/
    NvSciBufAttrListFree(attrListBuffer);
    NvSciBufAttrListFree(attrListReconciledBuffer)
    NvSciBufAttrListFree(attrListConflictBuffer);
    NvSciBufObjFree(bufferObjRaw);

Nvmedia内部没有具体的架构资料,不过通过上面的分析可以猜测到。通过attribute来验证内存输入输出的一致性(大小,数据类型要求,对齐等)后,创建局部加速器组 可见的对象(内部对地址SMMU的翻译是通过API来约定的)。没有约定的不能进行共享。

二: Stream数据流水处理

NVSciBuf只能满足两个异构计算单元的内存共享问题,但没有解决数据Pipeline的问题。如果要做Pipeline,肯定会涉及到持续的生产,消费,同步,丢弃等问题。为了统一SOC内部数据概念,让异构器件处理数据能够流动起来。NvMedia又引入了几个类。

  • NvSciStream
  • NvSciSync
  • NvSciSyncFence

我们通过一个例子来学习Nvidia实现这些类背后的一些概念。 这是一个位于SOC1上的B进程生产者,有三个消费者的数据流水线:

  • 路径1 — 位于SOC1上的B进程内的消费者,与生产者同进程。
  • 路径2 —位于SOC1上的A进程内的消费者。
  • 路径3 —位于SOC2上的C进程内的消费者。

Nvidia在处理流水线设计时,使用构建块(Building Block)的概念,即每一个逻辑对象都是块。下图中的每一个方块都是Block,以NvSciStreamBlock类来抽象,每一个独立方块都是该类具现的实体。 为什么这么做? 我猜测本身就是遵循组件化的思维,从DriveOS的代码框架、ISAAC的代码框架都可以看出来。这种基于Building Block的构建思维有利于在Component中复用。

pipeline

生产者拥有: (通过 NvSciErro NvSciStreamProducerCreate(NvSciStreamBlock pool, NvSciStreamBlock* producer) 创建生产者 )

  • Pool : 有静态池和动态池之分。功能安全场景只允许用静态池,非功能安全场景(如视屏播放)用动态池。
    • 对应API c++ NvSciErro NvSciStreamStaticPoolCreate(uint32_t numPacket NvSciStreamBlock* pool)
  • Multicast Block:用于多播。
    • 对应API NvSciError NvSciStreamMulticastCreate(uint32_t numOutputs,NvSciStreamBlock* multicast)
  • 共享内存IPC:利用NvSciBuf的共享内存通信。也是一个StreamBlock实现。
  • MemoryBoundary IPC: 利用拷贝的通信
    • IPC创建方式:NvSciError NvSciStreamIpcSrcCreate( NvSciIpcEndpoint ipcEndpoint, NvSciSyncModule syncModule, NvSciBufModule bufModule, NvSciStreamBlock* ipc) NvSciError NvSciStreamIpcDstCreate( NvSciIpcEndpoint ipcEndpoint, NvSciSyncModule syncModule, NvSciBufModule bufModule, NvSciStreamBlock* ipc)

消费者拥有:(通过 NvSciError NvSciStreamConsumerCreate( NvSciStreamBlock queue, NvSciStreamBlock* consumer) 创建消费者 )

  • QueueBlock: 支持FIFO和MailBox两种。
    • FIFO模式下,生产者必须等待消费者消费完当前帧。如果消费者速度很慢,生产者会降低生产的速度。producer阻塞在消费者的fence上。新数据可能会丢失。 对应API NvSciError NvSciStreamFifoQueueCreate(NvSciStreamBlock* queue)
    • MailBox模式下,生产者会循环利用buffer,将最新的数据代替消费还未消费的上一帧数据,让消费者一直用到最新的数据。对应API NvSciError NvSciStreamMailboxQueueCreate(NvSciStreamBlock* queue)
  • Pool : 有静态池和动态池之分。功能安全场景只允许用静态池,非功能安全场景(如视屏播放)用动态池。
  • 共享内存IPC:利用NvSciBuf的共享内存通信。
  • MemoryBoundary IPC: 利用拷贝的通信

构建好以上的数据结构,将producer和consumer的数据流转起来只需要:

NvSciError
NvSciStreamBlockConnect(
    NvSciStreamBlock upstream,
    NvSciStreamBlock downstream)

为了驱动数据流往后流动, 在创建好StreamBlock,将这些StreamBlock连接起来后。必须有事件在背后驱动数据流

typedef struct {
    NvSciStreamEventType   type;
    NvSciStreamSyncAttr    syncAttr;
    NvSciStreamSyncDesc    syncDesc;
    NvSciStreamElementAttr packetAttr;
    NvSciStreamPacket      packetCreate;
    NvSciStreamElementDesc packetElement;
    NvSciStreamCookie      packetCookie;
    NvSciError             error;
    uint32_t               index;
    uint32_t               count;
} NvSciStreamEvent

Nvidia 支持两种事件流驱动。在 5.1.6.1L 中,只支持了pool轮训式驱动,在没有数据时会浪费较多CPU。这种实现容易实现,只要在各个线程里主动取上下游的NvSciStreamBlock即可。 接口如下:

NvSciError
NvSciStreamBlockEventQuery(
    NvSciStreamBlock  block,
    int64_t           timeout_usec,
    NvSciStreamEvent *event)

在5.1之后引入了事件通知机制。从描述看,SDK开放使用Nvidia已有的NvSciIpc事件和操作系统已有的通知机制。

compatible with NvSciIpc events and operating system specific event mechanisms. allowing the thread to wait for more than just NvSciStream events at the same time. This mechanism is not supported in the current release.

老版本已经可以看得到的事件有:

  • NvSciStreamEventType_ConnectUpstream
  • NvSciStreamEventType_ConnectDownStream
  • NvSciStreamEventType_DisconnectUpstream
  • NvSciStreamEventType_DisconnectDownstream
  • NvSciStreamEventType_SyncAttr

值得一提的时,Nvidia非常注重功能安全场景的应用。在功能安全认证系统中,为了避免failure和资源销毁的误操作带来的潜在功能安全隐患,Nvidia打算移除所有Disconnect事件。

In safety-certified systems, failures and teardown are never supposed to occur. Although disconnect events are currently supported in the current safety release, they will likely be removed in later safety releases, and only supported for non-safety platforms.

至此,我们至少把数据的逻辑关系关联起来。

这还远远不够。

在SOC这种负责的异构器件里,存在CPU,GPU和其他加速引擎,他们在同时工作。 共享内存带来了效率提升,也带来同时访问数据的问题。

我们需要类似操作系统的同步原语(synchronization primitives)。Nvidia引入了

  • NvSciSync
    • fence: 代表拥有者的状态机,在多个sync对象中共享。状态机没有满足要求,动作被pending
    • sync对象:可以拥有多个fence
    • 等待fence的对象,称作sync对象上下文中的waiter。
  • NvSciSyncAttrList
    • 表示同步对象可以发送消息的属性需求
    • 表示同步对象可以接收消息的属性需求。

设置一个流的同步属性,同时

typedef struct {
    // Flag indicating fences are not supported
    bool              synchronousOnly;
    // The read requirements for sync objects
    NvSciSyncAttrList waiterSyncAttr;
} NvSciStreamSyncAttr
 
// 设置StreamBlock的同步属性
NvSciError
NvSciStreamBlockSyncRequirements(
    NvSciStreamBlock           block,
    const NvSciStreamSyncAttr* attr)
    
    

在定义好同步对象后。

分配Buffer,并创建Packet(Packet是一系列NvSciBuf的组合)

typedef struct {
    uint32_t               index;
    uint32_t               type;
    NvSciStreamElementMode mode;
    NvSciBufAttrList       bufAttr;
} NvSciStreamElementAttr
// 待补充用途 
NvSciError
NvSciStreamBlockPacketElementCount(
    NvSciStreamBlock block,
    uint32_t         count
)
// 待补充用途  
NvSciError
NvSciStreamBlockPacketAttr(
    NvSciStreamBlock              block,
    const NvSciStreamElementAttr* attr
)

typedef struct {
    NvSciStreamCookie cookie;
    NvSciSyncFence*   prefences;
} NvSciStreamPayload

// 从生产者中获取一个数据包 API
NvSciError
NvSciStreamProducerPacketGet(
    NvSciStreamBlock    producer,
    NvSciStreamPayload* payload)

  
typedef struct {
    uint32_t          index;
    NvSciStreamPacket handle;
    NvSciBufObj       buffer;
} NvSciStreamElementDesc
    
// 发端创建Packet API 
NvSciError
NvSciStreamPoolPacketCreate(
    NvSciStreamBlock   pool,
    NvSciStreamCookie  cookie,
    NvSciStreamPacket* handle
)
// 将数据插入Packet的API
NvSciError
NvSciStreamPoolPacketInsertBuffer(
    NvSciStreamBlock              pool,
    const NvSciStreamElementDesc* desc
)
    
// 生产者发送一个Packet
NvSciError
NvSciStreamProducerPacketPresent(
    NvSciStreamBlock  producer,
    NvSciStreamPacket handle,
    NvSciSyncFence*   postfences  //同步对象
)
    
// 消费者接收一个数据对象
 NvSciError
NvSciStreamConsumerPacketAcquire(
    NvSciStreamBlock    consumer,
    NvSciStreamPayload* payload
) 
    
    

Rust交叉编译

Rust提供的编译工具链很好用。

但文档上支撑还是有些问题。

在 .cargo/config 下配置为:

[target.aarch64-unknown-linux-gnu]
linker = "/home/HwHiAiUser/toolchain/toolkit/toolchain/hcc/bin/aarch64-target-linux-gnu-gcc"

生成二进制的格式为:

ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, 
for GNU/Linux 3.7.0, with debug_info, not stripped

如果.cargo/config 配置为:

[target.aarch64-unknown-linux-gnu]
linker = "/home/HwHiAiUser/toolchain/toolkit/toolchain/hcc/bin/aarch64-target-linux-gnu-ld"

生成二进制的格式为:

node-43b020df73fb0fbc: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-,
 with debug_info, not stripped

LLVM札记1: 架构

LLVM作者在文章(LLVM: A Compilation Framework for Lifelong Program Analysis & Transformation)对LLVM做了全面的Review。 指出LLVM是对现在JVM和托管C++的高层虚拟机的补充。是指令级别的,在IR层面和架构设计上有创新设计的。具体表现在将前端,中端,后端区分开来,做到编译时,链接时,安装时,运行时的优化。 传统的GCC构建框架,前端和后端代码是强耦合的。而用LLVM实现的编译器在前端Parser翻译好AST后,直接转换成统一的LLVM IR。在编译优化阶段通过Operation抽象的具体实现对语言进行不同处理,很好的体现了面向对象和模块化思维。

  1. 比如:TensorFlow有很多后端支持,以适配不同硬件需要。如TPU下的XLA,移动设备下的TFLite。
  2. 适配硬件的过程中,很多编译优化手段是通用的。没必要重写一遍。
  3. 针对这个问题,LLVM祭出MLIR来解决这个问题。比如A语言实现了某个常量折叠的优化。那么B语言可以复用A语言的优化时用到的数据结构。具体表现在继承一个表结构,复用操作过程。为什么可以共用,因为共享一个统一的中间层表示。你可以在插入多个IR(也是Multiple Layer Intermediate Representation的来源)来表示复杂的处理过程。这就是万能的软件中间层。
  4. LLVM IR是最底层的IR表示,它的设计创新在与可以对指针以SSA指令的方式来做算数操作。
  5. LLVM的另一个创新在于,自带一个类型系统。这个类型系统是抽象的,与上层具体语言无关的。

MLIR大概过程如下,LLVM IR作为底层表示,MLIR中间由用户扩展表示,并可以使用LLVM IR的数据结构。

mlir architecture
摘自文章

如果要分,其实还有中端。以下引用知乎的一段话”中端是最讲算法的部分,图的连通性,深度和广度优先搜索,强连通分量,最短路,拓扑排序这些基本算法全都用得上。后端思维方式是硬件思维,代码需要嵌入大量硬件相关的信息,并且有很多后端相关的优化。后端的优化相对中端更局部一些。”

MLIR目前只是个DSL工厂,社区里有各行各业的人: 硬件从业者,学者,工程师。具体分为研究算法的,设计新硬件的,做verification的,做ML的。

LLVM IR指令集:

  • 通过SSA的形式表达数据流,可以很清楚的定位代码的数据走向。
  • 显式的表达控制流(包括异常)
  • 显式的表达语言无关的类型信息
对于C代码
for (i = 0;i < N; ++i)
    Sum(&A[i],&P);

用LLVM IR来表达的样子是:

loop:
  // phi 指令位于基本块的开头,第一组是初始值,后面的组可以是label。
  %i.1 = phi int [ 0, %bb0 ], [ %i.2, %loop ]
  // 拿到 A指针 往后 偏移 i 的指针 即&A[i]
  %AiAddr = getelementptr float* %A, int %i.1
  // 调用Sum函数,这里的表示虽然是SSA形式,但表现形式很High Level
  call void %Sum(float %AiAddr, %pair* %P)
  // i变量自增1
  %i.2 = add int %i.1, 1
  //  如果 i 小于 N,那么将结果给 tmp.4 
  %tmp.4 = setlt int %i.1, %N
  // 条件跳转,如果tmp.4 为真,跳转到loop,如果为假,跳出循环。
  br bool %tmp.4, label %loop, label %outloop

LLVM札记2:基本块

BasicBlock:是一个顺序执行的指令容器。BasicBlock是一个Value,被分支和SWITCH语句所引用。 BasicBlock的类型是Type::LabelTy,这个类型代表一个branch分支可以跳转到的标签Label。一个组织良好的基本块只有在结束时才会被中断指令停止。但组织异常的基本块也会出现在构造和修改程序的中间态中出现,验证者需要确保基本块是“组织良好的”。

class BasicBlock final :
         public Value, // Basic blocks are data objects also
         public ilist_node_with_parent<BasicBlock, Function> {
 public:
   using InstListType = SymbolTableList<Instruction>;
 
 private:
   friend class BlockAddress;
   friend class SymbolTableListTraits<BasicBlock>;
 
   InstListType InstList;
   Function *Parent;
}

那么对于下面这段程序,他有多少个基本块呢。

int main(int argc, char **argv) {
  int i, j, k, t = 0;
  for(i = 0; i < 10; i++) {
    for(j = 0; j < 10; j++) {
      for(k = 0; k < 10; k++) {
        t++;
      }
    }
    for(j = 0; j < 10; j++) {
      t++;
    }
  }
  for(i = 0; i < 20; i++) {
    for(j = 0; j < 20; j++) {
      t++;
    }
    for(j = 0; j < 20; j++) {
      t++;
    }
  }
  return t;
}

利用LLVM中的BlockFrequencyInfo类的来统计下基本块信息:

//runOnFunction 
bool BlockFrequencyInfoWrapperPass::runOnFunction(Function &F) {
  BranchProbabilityInfo &BPI =
      getAnalysis<BranchProbabilityInfoWrapperPass>().getBPI();
  LoopInfo &LI = getAnalysis<LoopInfoWrapperPass>().getLoopInfo();
  BFI.calculate(F, BPI, LI);
  return false;
}

以上代码会调用LoopInfo Pass来获取循环的信息,如果我们迭代的从这个对象里获取信息,我们可以拿到基本块信息:

unsigned num_Blocks = 0;
  Loop::block_iterator bb;
  for(bb = L->block_begin(); bb != L->block_end();++bb)
    num_Blocks++;
  errs() << "Loop level " << nest << " has " << num_Blocks
<< " blocks\n";

上面代码拿到的只是外层循环的基本块。为了获取内存循环的基本块信息,可以递归的调用 `getSubLoops`函数。

void countBlocksInLoop(Loop *L, unsigned nest) {
  unsigned num_Blocks = 0;
  Loop::block_iterator bb;
  for(bb = L->block_begin(); bb != L->block_end();++bb)
    num_Blocks++;
  errs() << "Loop level " << nest << " has " << num_Blocks
<< " blocks\n";
  std::vector<Loop*> subLoops = L->getSubLoops();
  Loop::iterator j, f;
  for (j = subLoops.begin(), f = subLoops.end(); j != f;
++j)
    countBlocksInLoop(*j, nest + 1);
}
 
virtual bool runOnFunction(Function &F) {
  LoopInfo *LI = &getAnalysis<LoopInfoWrapperPass>().getLoopInfo();
  errs() << "Function " << F.getName() + "\n";
  for (Loop *L : *LI)
    countBlocksInLoop(L, 0);
  return false;
}

通过用Opt优化ll代码,我们自定义的Pass会被执行

clang –O0 –S –emit-llvm sample.c –o sample.ll
opt  -load (path_to_.so_file)/FuncBlockCount.so  -funcblockcount sample.ll

输出:

Function main
Loop level 0 has 11 blocks
Loop level 1 has 3 blocks
Loop level 1 has 3 blocks
Loop level 0 has 15 blocks
Loop level 1 has 7 blocks
Loop level 2 has 3 blocks
Loop level 1 has 3 blocks

输出很不好理解,而且好像人工也算不出。看不出来基本块是怎么计算出来的。

理工男的打理

理工男准备写点购物备忘。想到的第一句话竟然是:电子商务冲击实体店。还有救吗?

言归正传,一直觉得自己不会穿衣服。最近在淘宝上买了几件衣服和裤子,都不太如意。主要原因呢是伊对自己的尺码不太清楚。也不会搭配再加之,再加上身体不太瘦。(发福就发福嘛,还不好意思承认。)

第一在网上看了很多七龙珠主题相关的动漫衣服。觉得到了这个年纪穿这种装嫩的衣服不太合适。但还是架不住童年的回忆,买了几件。第二买了几套工装的裤子和衣服。这些尺码不太让人满意,又做了一次更换。第三买了一些工装的靴子。可能比较符合理工男的气质。(终于不要,总是格子衬衫啦。)

好,开始总结一下。

裤子呢,穿的太大了。像一个装修工人一样,不太修身也不太彰显气质。总之,买家秀和卖家秀的差别比较大,这真是一个令人头疼的问题啊。裤子呢穿XL可能会比较休闲,但是要注意腰身要提上。如果是L码的话呢,又怕私处受到挤压。(修身到底是个什么概念?我还没搞清楚!)。难道一定要去裁缝店量身定做?腰围到底有多少呢?腿长有多少呢?裤脚可不可以折起来呢?折起来好不好看呢?裤子的版型到底有多少种?理工男应该给他一个参数规格列表。到私处的长度是多少,应该再给个参数吧?这个大多数淘宝店是没有明确说明的,需要自己去尝试。(或者裁缝要知道)。

第二呢,买了一件工装的衬衫。衬衫挺不错的,条纹状,蓝白相间的竖条纹。选的型号是XL,淘宝的评论里面有人说这个穿起来像病号服(大笑😄)。但是穿在我身上感觉挺不错的。我想买衬衫以后就是XL吧,L的那肯定是不标准的尺码。第1次用心的买衣服,应该以后会更有经验,加油。

第三买了一些袜子,这些袜子大多数是有卡通主题的。也算是还了童年的一个愿望吧。据某人说袜子是消耗品,随便买买就可以了。可惜买东西上面我还真不是随随便便的人。

最后就是私人的内裤了,我毫不犹豫的买了一些CK的内裤。照道理,内裤应该也是消耗品,但是…我虚荣心作祟。我想,内裤以后可以买一些国产的品牌就不错了。

买完衣服后下个总结:最需要的是一个软尺,测量好胸围,腰围,臀围,腿长肩宽(🙃)。然后就是增强锻炼。让身体看起来更有版型,更有气质。摆脱理工男在人们心目中的形象。

补充:买了正版的350 V2的Sand Topae和 Nike 990元祖灰,同时买了莆田的350 V2, 虽然外观和做工都非常真了,但脚感还是稍有区别(退

把工位做了收纳,每天在工位至少8个小时。 几乎和床一样重要的东西了。

至此:床,鞋,衣服,袜子,工位,键盘。这几样和我相处最常的物件,我已经将你们打理好了。公司的椅子1000来块的这条件暂时不期望去改变了。在家里好好搞搞吧。

第三次拳击课

教练Artem是一个俄国大汉,嘟囔几句简单的英语。用形体语言给学员基本的示范。

前几次体能无法跟进。每次上完课,人处在一个万念俱空的状态。以至于过了好几天,走路时偶然的瑟瑟发抖。

通过有意识的对体能进行恢复(坚持每天跳绳15分钟+拉伸15分钟,直到出汗为止)。今天基本上能跟上节奏。对运动有了一些好感。

最大的感受是,拳击和网球一样,在入门时需要注重腰部力量。扭腰过程中,腿部发力,带动全身。网球是挥出球拍,拳击则是打中别人的脸 😄。

费曼阅读和記憶方法

這幾天看到 劉同學 的知乎。雖然口吻稍有誇張,但著實描述了自己的學習方法。記憶無非是大腦中按圖索驥,一點點的還原。

怎麼讓大腦的神經元關聯起來,快速的從記憶中獲取所需要的信息。

費曼的做法碰到不懂的地方就從頭來讀。

劉的做法是,按照順序,按照知識的相關性反覆記憶。

都是加強神經元的鏈接。方法很多,一定要找到適用自己的。