[4.2-2] NCCL新版本的register如何实现的?

文章目录

    • 1->2->3
    • 1. ncclRegisterP2pIpcBuffer
    • 2. ncclIpcLocalRegisterBuffer(..., 1, 0,...)
    • 3. ipcRegisterBuffer(..., regRecord,..., isLegacyIpc)
    • 4. p2pProxyRegister()

1->2->3

1. ncclRegisterP2pIpcBuffer

在enqueue.cc内的调用是:

NCCLCHECK(ncclRegisterP2pIpcBuffer(comm, addrs[dir], bytes[dir], peerRank, &regFlag, &regAddr, &plan->cleanupQueue));

会走到sendrecv_reg.cc,这里的 ncclRegisterP2pIpcBuffer 实现:

  • ncclParamLocalRegister()环境变量开启的话就会走到ncclIpcLocalRegisterBuffer,这里的NCCL_IPC_SENDRECV是0,最重要的就是这里的regAddr。返回出来的 regAddr 地址是对端的地址加上偏移peerRmtAddrs + offset;
  • ncclIpcGraphRegisterBuffer类似
ncclResult_t ncclRegisterP2pIpcBuffer(struct ncclComm* comm,           // NCCL 通信器,包含所有通信上下文信息void* userbuff,                  // 用户要注册的 buffer 地址(本地 buffer)size_t size,                     // buffer 的大小(字节数)int peerRank,                    // 对端 rank ID(要与哪个 rank 进行 P2P 通信)int* regFlag,                    // 输出参数:注册是否成功的标志(0=失败,1=成功)void** regAddr,                  // 输出参数:注册后获得的远程地址(对端 buffer 地址)struct ncclIntruQueue<struct ncclCommCallback, &ncclCommCallback::next>* cleanupQueue  // 清理队列,用于管理资源释放
) {ncclResult_t ret = ncclSuccess;uintptr_t offset = 0;uintptr_t* peerRmtAddrs = NULL;*regFlag = 0;if (comm->planner.persistent && ncclParamGraphRegister()) {ncclIpcGraphRegisterBuffer(comm, userbuff, size, &peerRank, 1, NCCL_IPC_SENDRECV, regFlag, &offset, &peerRmtAddrs, reinterpret_cast<void*>(cleanupQueue), NULL);}if (*regFlag == 0 && ncclParamLocalRegister()) {ncclIpcLocalRegisterBuffer(comm, userbuff, size, &peerRank, 1, NCCL_IPC_SENDRECV, regFlag, &offset, &peerRmtAddrs);}if (*regFlag)*regAddr = (void*)((uintptr_t)peerRmtAddrs + offset);return ret;
}

2. ncclIpcLocalRegisterBuffer(…, 1, 0,…)

在调用真正的 ipcRegisterBuffer 之前,多创建了一个 ncclReg 结构的 regRecord 去:

  1. 查找是否已经注册( ncclRegFind(comm, userbuff, buffSize, &regRecord) )
  2. 注册过的地址是否有效( ncclRegLocalIsValid(regRecord, &isValid) )

ncclResult_t ncclIpcLocalRegisterBuffer(ncclComm* comm,                    // NCCL 通信器const void* userbuff,              // 用户要注册的 buffer 地址size_t buffSize,                   // buffer 大小int* peerRanks,                    // 对端 rank 数组int nPeers,                        // 对端数量 这里上面丢下来的是1ncclIpcRegType type,               // 注册类型 (NCCL_IPC_SENDRECV=0)int* regBufFlag,                   // 输出:注册成功标志uintptr_t* offsetOut,              // 输出:buffer 在注册内存中的偏移uintptr_t** peerRmtAddrsOut        // 输出:对端远程地址数组
) {ncclResult_t ret = ncclSuccess;struct ncclReg *regRecord = NULL;bool isValid = false;*regBufFlag = 0;*offsetOut = 0;*peerRmtAddrsOut = NULL;if (comm && userbuff && buffSize > 0 && nPeers > 0) {NCCLCHECKGOTO(ncclRegFind(comm, userbuff, buffSize, &regRecord), ret, fail);NCCLCHECKGOTO(ncclRegLocalIsValid(regRecord, &isValid), ret, fail);if (isValid)NCCLCHECKGOTO(ipcRegisterBuffer(comm, userbuff, buffSize, peerRanks, nPeers, type, regRecord, regBufFlag, offsetOut, peerRmtAddrsOut, NULL), ret, fail);}exit:return ret;
fail:*regBufFlag = 0;*offsetOut = 0;*peerRmtAddrsOut = NULL;goto exit;
}

3. ipcRegisterBuffer(…, regRecord,…, isLegacyIpc)

跨process P2P通信内存映射的实现,本地进程可以直接access对端进程的内存。

  1. 首先则立初始化了局部变量 还有要返回的变量
static ncclResult_t ipcRegisterBuffer(ncclComm* comm, const void* userbuff, size_t buffSize, int* peerRanks, int nPeers, ncclIpcRegType type, struct ncclReg* regRecord, int* regBufFlag, uintptr_t* offsetOut, uintptr_t** peerRmtAddrsOut, bool* isLegacyIpc) {// 初始化局部变量ncclResult_t ret = ncclSuccess;struct ncclIpcRegInfo* newInfo = NULL;      // 新的 IPC 注册信息uintptr_t* peerRmtAddrs = NULL;             // 对端远程地址数组int legacyIpcCap = 0;                       // Legacy IPC 能力标志size_t baseSize = 0;                        // 基地址大小void* baseAddr = NULL;                      // 基地址bool needUpdate = false;                    // 是否需要更新设备端地址数组// 初始化所有输出参数为默认值*regBufFlag = 0;*offsetOut = 0;*peerRmtAddrsOut = NULL;if (isLegacyIpc) *isLegacyIpc = false;
  1. 主注册loop,这里会遍历所有对端。主要是global rank号转换成local rank号
if (regRecord) {int peerLocalRank = -1;for (int p = 0; p < nPeers; p++) {int peerRank = peerRanks[p];                           // 全局的对端rankpeerLocalRank = comm->rankToLocalRank[peerRank];       // 转换为本地rank
  1. 检查是否在regRecord内注册过了,如果注册过的话,直接去复用
if (regRecord->ipcInfos[peerLocalRank]) {// We already have IPC info for peerLocalRank, no need to register it, we can reuse it*regBufFlag = 1;if (isLegacyIpc) *isLegacyIpc = regRecord->ipcInfos[peerLocalRank]->impInfo.legacyIpcCap;INFO(NCCL_REG, "rank %d - IPC reuse buffer %p size %ld (baseAddr %p size %ld) to peer %d regAddr %p", comm->rank, userbuff, buffSize, (void*)regRecord->addr, regRecord->pages * comm->regCache.pageSize, peerRank, regRecord->ipcInfos[peerLocalRank]->impInfo.rmtRegAddr);
}
  1. 没注册的话就开始注册,这一步大致包括获取buff信息->建立proxy连接->创建IPC Handle:
// Register buffer with peerLocalRankstruct ncclProxyConnector* proxyConn = NULL;struct p2pIpcExpInfo ipcInfo;if (baseAddr == NULL) {CUCHECKGOTO(cuMemGetAddressRange((CUdeviceptr*)&baseAddr, &baseSize, (CUdeviceptr)userbuff), ret, fail);CUCHECKGOTO(cuPointerGetAttribute((void*)&legacyIpcCap, CU_POINTER_ATTRIBUTE_IS_LEGACY_CUDA_IPC_CAPABLE, (CUdeviceptr)baseAddr), ret, fail);}if (comm->gproxyConn[peerRank].initialized == false)NCCLCHECKGOTO(ncclProxyConnect(comm, TRANSPORT_P2P, 1, peerRank, &comm->gproxyConn[peerRank]), ret, fail);proxyConn = &comm->gproxyConn[peerRank];// Get the mem handle for that buffer. It may have been allocated through cudaMalloc in which case we'll// get the CUDA legacy mem handle, or through cuMem*.if (ncclCuMemEnable()) {CUmemGenericAllocationHandle handle;if (CUPFN(cuMemRetainAllocationHandle(&handle, baseAddr)) != CUDA_SUCCESS) {// if cuMem* export fails, retry legacy exportif (comm->directMode || !ncclParamLegacyCudaRegister()) goto fail;CUDACHECKGOTO(cudaIpcGetMemHandle(&ipcInfo.ipcDesc.devIpc, baseAddr), ret, fail);ipcInfo.legacyIpcCap = true;if (isLegacyIpc) *isLegacyIpc = true;} else {ipcInfo.legacyIpcCap = false;if (isLegacyIpc) *isLegacyIpc = false;// cuMem* export to file descriptor or fabric handleif (proxyConn->sameProcess) {memcpy(&ipcInfo.ipcDesc.memHandle, &handle, sizeof(CUmemGenericAllocationHandle));} else {if (ncclCuMemHandleType == CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR) {// **** 这里是最最最最最最重要的部分********int expFd = -1;// 这里的cuMem的handle导出成文件描述符expFdCUCHECKGOTO(cuMemExportToShareableHandle(&expFd, handle, ncclCuMemHandleType, 0), ret, fail);// 发送expFd到对端的进程,调用UDSNCCLCHECKGOTO(ncclProxyClientQueryFdBlocking(comm, proxyConn, expFd, &ipcInfo.impFd), ret, fail);SYSCHECKGOTO(close(expFd), "close", ret, fail);} else {// Allow this to silently fail for cases where the user buff cannot be registeredif (CUPFN(cuMemExportToShareableHandle(&ipcInfo.ipcDesc.cuDesc.handle, handle, ncclCuMemHandleType, 0)) != CUDA_SUCCESS) {CUCHECKGOTO(cuMemRelease(handle), ret, fail);goto fail;}}}CUCHECKGOTO(cuMemRelease(handle), ret, fail);}} else if (legacyIpcCap) {// legacy exportif (comm->directMode || !ncclParamLegacyCudaRegister()) goto fail;CUDACHECKGOTO(cudaIpcGetMemHandle(&ipcInfo.ipcDesc.devIpc, baseAddr), ret, fail);ipcInfo.legacyIpcCap = true;if (isLegacyIpc) *isLegacyIpc = true;} else {// nothing works, just returngoto fail;}
  • ncclProxyConnect(comm, TRANSPORT_P2P, 1, peerRank, &comm->gproxyConn[peerRank]), ret, fail);
  • ncclProxyCallBlocking(comm, proxyConn, ncclProxyMsgRegister, &ipcInfo, sizeof(p2pIpcExpInfo), &rmtRegAddr, sizeof(void*)), ret, fail);
    两次触发proxy,都会对应的去走 p2pTransport 内的 p2pSendProxyConnectp2pProxyRegister,还有 p2pProxyDeRegister。 这里请跳到后面4章我单独说明了 p2pProxyRegister
flowchart TDA[获取 buff 信息] --> B[ncclProxyConnect] --> C[创建 IPC Handle]C --> D{cuMem Enable?}D -- 否 --> E{允许 legacy?}E -- 否 --> Z[失败:直接返回 fail]E -- 是 --> F[使用 cudaIpcGetMemHandle 获取 legacy IPC]F --> G[设置 legacyIpcCap = true]D -- 是 --> H[cuMemRetainAllocationHandle 成功?]H -- 否 --> I{允许 legacy?}I -- 否 --> ZI -- 是 --> FH -- 是 --> J{是否是 sameProcess?}J -- 是 --> K[直接 memcpy handle]J -- 否 --> L{Handle 类型?}L -- POSIX_FD --> M[导出到 fd → proxyClientQueryFd → close fd]L -- 其他类型 --> N[cuMemExportToShareableHandle → 检查 → 释放 handle]M --> O[释放 handle]N --> OK --> OO --> P[设置 legacyIpcCap = false]
  1. 向对端注册并获取远程地址
    在第4步中一开始就在向 p2pIpcExpInfo 结构的 ipcInfo 中填写接下来注册需要的信息。然后 ncclProxyConnector 结构的proxyConn内有连接的信息。这两者包含了注册所需要的所有信息,通过具体p2p传输层的proxy向对端注册,并拿到对端proxy返回的注册地址 rmtRegAddr
void* rmtRegAddr = NULL;ipcInfo.size = baseSize;// offset是用户注册的内存区域在完整内存块中的偏移,给后面保存主测信息用ipcInfo.offset = regRecord->addr - (uintptr_t)baseAddr;// Now ipcInfo contains all necessary registration info. Start to register buffer on proxy side// and get the remote register address back.if (proxyConn) {INFO(NCCL_REG, "rank %d - IPC registering buffer %p size %ld (baseAddr %p size %ld) to peer %d", comm->rank, userbuff, buffSize, (void*)regRecord->addr, ipcInfo.size, peerRank);NCCLCHECKGOTO(ncclProxyCallBlocking(comm, proxyConn, ncclProxyMsgRegister, &ipcInfo, sizeof(p2pIpcExpInfo), &rmtRegAddr, sizeof(void*)), ret, fail);}
  1. 保存注册信息
if (rmtRegAddr) {NCCLCHECKGOTO(ncclCalloc(&newInfo, 1), ret, fail);// 更新注册记录状态regRecord->state |= IPC_REG_COMPLETE;// 填充注册信息newInfo->peerRank = peerRank;newInfo->baseAddr = baseAddr;newInfo->impInfo.rmtRegAddr = rmtRegAddr;        // 远程地址!newInfo->impInfo.offset = ipcInfo.offset;newInfo->impInfo.legacyIpcCap = ipcInfo.legacyIpcCap;newInfo->ipcProxyconn = proxyConn;// 保存到 regRecordregRecord->ipcInfos[peerLocalRank] = newInfo;// 初始化主机端地址数组if (regRecord->regIpcAddrs.hostPeerRmtAddrs == NULL) {NCCLCHECKGOTO(ncclCalloc(&regRecord->regIpcAddrs.hostPeerRmtAddrs, comm->localRanks), ret, fail);}regRecord->regIpcAddrs.hostPeerRmtAddrs[peerLocalRank] = (uintptr_t)rmtRegAddr;needUpdate = true;*regBufFlag = 1;  // 标记注册成功
}
  1. 返回peer对的地址
  • p2p走的else分支,直接把对端的地址写到peerRmtAddrsOut指针内,由 ipcRegisterBuffer 返回给调用方
  • cc则会维护所有对端地址数组
if (*regBufFlag) {if (type == NCCL_IPC_COLLECTIVE) {if (regRecord->regIpcAddrs.devPeerRmtAddrs == NULL || needUpdate) {// 获取 CUDA 流cudaStream_t hostStream, deviceStream;NCCLCHECKGOTO(ncclStrongStreamAcquire(ncclCudaGraphNone(), &comm->sharedRes->hostStream, false, &hostStream), ret, fail);NCCLCHECKGOTO(ncclStrongStreamAcquire(ncclCudaGraphNone(), &comm->sharedRes->deviceStream, false, &deviceStream), ret, fail);// 分配设备端地址数组if (regRecord->regIpcAddrs.devPeerRmtAddrs == NULL)NCCLCHECKGOTO(ncclCudaCallocAsync(&regRecord->regIpcAddrs.devPeerRmtAddrs, comm->localRanks, hostStream), ret, fail);// 将主机端地址数组复制到设备端if (needUpdate)NCCLCHECKGOTO(ncclCudaMemcpyAsync(regRecord->regIpcAddrs.devPeerRmtAddrs, regRecord->regIpcAddrs.hostPeerRmtAddrs, comm->localRanks, hostStream), ret, fail);// 同步流NCCLCHECKGOTO(ncclStreamWaitStream(deviceStream, hostStream, comm->sharedRes->scratchEvent), ret, fail);NCCLCHECKGOTO(ncclStrongStreamRelease(ncclCudaGraphNone(), &comm->sharedRes->hostStream, false), ret, fail);NCCLCHECKGOTO(ncclStrongStreamRelease(ncclCudaGraphNone(), &comm->sharedRes->deviceStream, false), ret, fail);}peerRmtAddrs = regRecord->regIpcAddrs.devPeerRmtAddrs;} else {assert(nPeers == 1);// p2p always returns remote addr here since remote buffer addr is passed in ncclDevWorkP2p structpeerRmtAddrs = (uintptr_t*)regRecord->regIpcAddrs.hostPeerRmtAddrs[peerLocalRank];}*offsetOut = (uintptr_t)userbuff - regRecord->addr;*peerRmtAddrsOut = peerRmtAddrs;}

4. p2pProxyRegister()

ipcRegisterBuffer 触发proxy注册前是传递了:proxyConn和ipcInfo到ncclProxyMsgRegister内。

NCCLCHECKGOTO(ncclProxyCallBlocking(comm, proxyConn, ncclProxyMsgRegister, &ipcInfo, sizeof(p2pIpcExpInfo), &rmtRegAddr, sizeof(void*)), ret, fail);

具体来看:p2pProxyRegister实现:

  1. 首先就是void* reqBuff指针指向的地址,也就是上面传下来的ipcInfo。变为ipcExpInfo后从里面取需要的信息。也就是之前 ipcRegisterBuffer 计算得到的size,offset,用cuda ipc/cuMem,和ipc描述符(ipcDesc)。
static ncclResult_t p2pProxyRegister(struct ncclProxyConnection* connection,    // proxy 连接信息struct ncclProxyState* proxyState,        // proxy 状态void* reqBuff,                            // 请求缓冲区 (p2pIpcExpInfo)int reqSize,                              // 请求大小void* respBuff,                           // 响应缓冲区 (void* regAddr)int respSize,                             // 响应大小int* done                                 // 完成标志
) {struct p2pIpcExpInfo* ipcExpInfo = (struct p2pIpcExpInfo*)reqBuff;void* regAddr = NULL;ncclResult_t ret = ncclSuccess;bool mapped = false;bool imported = false;CUmemGenericAllocationHandle handle;assert(sizeof(struct p2pIpcExpInfo) == reqSize);   // 确认请求大小assert(sizeof(void*) == respSize);                 // 确认响应大小
}
  1. 如果走传统cuda ipc那么就是走:
if (ipcExpInfo->legacyIpcCap) {// legacy importCUDACHECKGOTO(cudaIpcOpenMemHandle(&regAddr, ipcExpInfo->ipcDesc.devIpc, cudaIpcMemLazyEnablePeerAccess), ret, fail);regAddr = (void*)((uintptr_t)regAddr + ipcExpInfo->offset);} else {

但是大部分都是cuMem,ipcExpInfo->legacyIpcCap是false的。所以会走cuMem的ipc:
这里的P2pProxyRegister运行于对端的Proxy进程,完成 cuMemImportFromShareableHandle + cuMemMap ,在对端jin

} else {// cuMem importif (connection->sameProcess) {// 同进程直接复制handlememcpy(&handle, &ipcExpInfo->ipcDesc.memHandle, sizeof(CUmemGenericAllocationHandle));} else {// 跨进程:需要导入句柄if (ncclCuMemHandleType == CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR) {// 文件描述符方式CUCHECKGOTO(cuMemImportFromShareableHandle(&handle, (void*)(uintptr_t)ipcExpInfo->impFd, ncclCuMemHandleType), ret, fail);SYSCHECKGOTO(close(ipcExpInfo->impFd), "close", ret, fail);} else {// Fabric Handle 方式CUCHECKGOTO(cuMemImportFromShareableHandle(&handle, (void*)&ipcExpInfo->ipcDesc.cuDesc, ncclCuMemHandleType), ret, fail);}}imported = true;// 接着就会去cuMem内存映射// 预留虚拟地址空间CUCHECKGOTO(cuMemAddressReserve((CUdeviceptr*)&regAddr, ipcExpInfo->size, /* alignment */ 0, /* addr */ 0, /* flags */ 0), ret, fail);// 将物理内存映射到虚拟地址CUCHECKGOTO(cuMemMap((CUdeviceptr)regAddr, ipcExpInfo->size, /* offset */ 0, handle, /* flags */ 0), ret, fail);mapped = true;// 设置访问权限CUmemAccessDesc accessDesc = {};accessDesc.location.type = CU_MEM_LOCATION_TYPE_DEVICE;accessDesc.location.id = proxyState->cudaDev;                // 本地 GPU IDaccessDesc.flags = CU_MEM_ACCESS_FLAGS_PROT_READWRITE;       // 读写权限CUCHECKGOTO(cuMemSetAccess((CUdeviceptr)regAddr, ipcExpInfo->size, &accessDesc, 1), ret, fail);// 加上偏移量,指向实际的用户 bufferregAddr = (void*)((uintptr_t)regAddr + ipcExpInfo->offset);
}
  1. 返回结果
    这里respBuff是proxy返回的参数,调用者 ipcRegisterBuffer 内的rmtRegAddr会拿到respBuff。
exit:memcpy(respBuff, (void*)&regAddr, sizeof(void*));  // 将 regAddr 复制到响应缓冲区*done = 1;                                          // 标记操作完成return ret;
## 5. summary
handle是**物理内存的标识符**,在 CUDA 统一内存管理中,物理内存和虚拟地址是分离的。
* **File Descriptor(FD)**:利用 Linux 内核的文件描述符机制,可以跨进程传递
* **Fabric Handle(FH)**:NVIDIA 的网络互连技术,支持跨节点的内存共享
```cpp
// rank1 进程导出 FD
int expFd;
cuMemExportToShareableHandle(&expFd, handle, CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR, 0);// 通过 Unix Domain Socket 发送到 rank2
ncclProxyClientQueryFdBlocking(comm, proxyConn, expFd, &impFd);// rank2 进程接收 FD 并导入
cuMemImportFromShareableHandle(&newHandle, &impFd, CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR);

![[sendrecv_reg 2025-08-05 17.02.47.excalidraw|100%]]
这个过程就是:

  1. rank1 的物理内存通过 handle 被 rank2 进程导入
  2. rank2 在自己的虚拟地址空间B中创建映射 [regAddr]
  3. [rmtRegAddr] 就是 rank2 进程中指向 rank1 内存的虚拟地址A
  4. rank1 告诉rank2:"要访问我的内存,请使用 rank2 进程中的地址 [rmtRegAdd]

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/918472.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/918472.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在idea中git切换分支,但是我的文件没add,没commit

这是一个很悲伤的故事&#xff0c;我朋友一个下午写了4个小时的代码&#xff0c;差不多10多个类&#xff0c;都在切换分支的时候。IDEA发现有冲突&#xff0c;然后就要resolve conflict&#xff0c;发现自己不知道怎么操作&#xff0c;就点了abort & rollback。然后所有代码…

GPFS api

一、核心命令行 API&#xff08;mm 命令集&#xff09; GPFS 最基础且常用的接口是命令行工具集&#xff08;以mm为前缀&#xff09;&#xff0c;用于文件系统的创建、配置、管理和监控。这些命令可直接在终端执行&#xff0c;也可通过脚本&#xff08;如 Shell、Python&#…

虚拟机一站式部署Claude Code 可视化UI界面

前言 最近&#xff0c;强大的 AI 编码助手 Claude Code 在开发者社区中迅速走红&#xff0c;凭借其出色的代码生成和理解能力赢得了广泛赞誉。然而&#xff0c;其纯粹基于命令行的交互方式&#xff0c;对于许多习惯了图形化界面的开发者&#xff0c;尤其是新手而言&#xff0c…

网站IP被劫持?三步自建防护盾

一、劫持检测实战&#xff08;Python脚本&#xff09; import requests import socket import ssldef check_hijacking(domain):try:# 获取真实DNS解析real_ip socket.gethostbyname(domain)# 本地发起请求验证response requests.get(f"https://{domain}", timeout…

SQL Server从入门到项目实践(超值版)读书笔记 23

第三篇 核心应用篇在本章中&#xff0c;将通过案例示范学习SQL Server数据库的一些核心应用。例如&#xff0c;SQL Server视图的使用、游标的应用、存储过程的应用、索引的应用、触发器的应用、SQL Server事务与锁的应用等。学完本篇&#xff0c;读者将对SQL Server数据库的管理…

功能测试中常见的面试题-一

一、基础概念与理论题什么是软件测试&#xff1f;它的目的是什么&#xff1f;回答&#xff1a; 软件测试是通过人工或自动化手段&#xff0c;运行或评估软件系统&#xff0c;以验证它是否满足规定的需求、识别实际结果与预期结果之间的差异&#xff0c;并评估软件产品质量的过程…

LINUX88 变量:命令定义;普通数组定义(复);declare -i /-x

问题 [codesamba ~]$ array3(ls axel-2.4) [codesamba ~]$ echo $array3 API [codesamba ~]$ ls axel-2.4 API CHANGES conn.o gui README tcp.o axel conf.c COPYING http.c ru.mo text.c axel.1 …

数字IC后端PPA优化| Timing一致性调整方法和Module Region规划方法

Q1:直播课经常讲到一致性&#xff0c;这个一致性的话一般是指place&#xff0c;CTS和PT的derating time&#xff0c;uncertainty和transition吗&#xff0c;我大概知道innovus的uncertainty设置要比PT里面高一点&#xff0c;但具体设计时这几部分的大小应该是一个什么样的关系或…

电子电气架构 --- 软件定义汽车的驱动和挑战

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…

机器学习——10 支持向量机SVM

1 支持向量机 1.1 故事引入看下图左边&#xff0c;蓝色和红色的点混在一起&#xff0c;这就像一堆数据&#xff0c;没办法用一条简单的直线把它们分开。再看下图右边&#xff0c;有一条直线把蓝色和红色的点分开&#xff0c;这就是SVM在找的“决策边界”&#xff0c;它能把不同…

若以微服务部署踩坑点

windows docker desktop 部署nacos mysql1、docker部署nacosdocker pull nacos/nacos-server:v2.4.3docker启动命令 docker run --name nacos -d -p 8848:8848 -p 9848:9848 -p 9849:9849 --privilegedtrue --network bridge -e MODEstandalone -e SPRING_DATASOURCE_PLATFORMm…

Lua基础+Lua数据类型

Lua基础 Lua介绍 特点&#xff1a;轻量、小巧。C语言开发。开源。 设计的目的&#xff1a;嵌入到应用程序当中&#xff0c;提供灵活的扩展和定制化的功能。 luanginx&#xff0c;luaredis。 环境安装 windows上安装lua&#xff1a; 检查机器上是否有lua C:\Users\cpf>lua lu…

基于VuePress2开发文档自部署及嵌入VUE项目

最近在搞前端开发帮助文档&#xff0c;转了一圈发现Vue提供了一个高性能的、Vue驱动的静态网站生成框架-VuePress。VuePress 是一个以 Markdown 为中心的静态网站生成器。你可以使用 Markdown 来书写内容&#xff08;如文档、博客等&#xff09;&#xff0c;然后 VuePress 会生…

Flask初步学习

文章目录一、初识Flask1.1 Pycharm修改环境配置1.2 运行第一个flask项目1.3 获取数据请求1.3.1 动态路由参数一、初识Flask 1.1 Pycharm修改环境配置 file——settings——project——python Interpreter——add interpreter——add local interpreter 1.2 运行第一个fla…

word的正则替换

word查看选中了几行 word替换掉空行 替换空行 按下 “Ctrl H” 组合键打开 “查找和替换” 对话框&#xff0c;在 “查找内容” 框中输入 “pp”&#xff0c;“^p” 代表段落标记&#xff0c;两个 “^p” 表示连续的两个段落标记&#xff0c;即空行。在 “替换为” 框中输入 “…

Spring Framework源码解析——DisposableBean

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl一、概述 DisposableBean 是 Spring 框架中用于定义 Bean 销毁时回调行为的核心接口之一。它提供了一个标准化的钩子方法 destroy()&#xff0c;允许 Bean 在容器关闭或作用域…

linux安装和使用git

Linux 上安装 Git 在 Linux 上安装 Git&#xff0c;你可以按照以下步骤进行&#xff1a; 打开终端&#xff1a;打开你的 Linux 终端应用程序。通常可以通过在应用程序菜单中搜索 "Terminal" 或 "终端" 来找到它。 更新软件包列表&#xff1a;运行以下命令…

数字图像处理4

预处理——ROI——形态学处理形态学处理形态学变化只能在二值图上处理1.腐蚀Erode对kernel映射的区域做与操作&#xff0c;包括自己在内如果有0则中间赋值成02.膨胀Dilate对kernel映射的区域做或操作&#xff0c;包括自己在内如果有1则中间赋值成13.其他操作开操作&#xff1a;…

Solon v3.4.3 发布(国产 Java 应用开发生态基座)

Solon 框架&#xff01; Solon 是新一代&#xff0c;Java 企业级应用开发框架。从零开始构建&#xff08;No Java-EE&#xff09;&#xff0c;有灵活的接口规范与开放生态。采用商用友好的 Apache 2.0 开源协议&#xff0c;是“杭州无耳科技有限公司”开源的根级项目&#xff…

Spring-Security-5.7.11升级6.5.2

1.Session Management 1.1.必须明确调用SecurityContextRepository保存SecurityContext 在Spring Security 5中&#xff0c;默认行为是SecurityContext使用SecurityContextPersistenceFilter自动保存到SecurityContextRepository。 //版本5.7.11 //SecurityContextPersisten…