CUDA中的图内存节点

news2025/6/4 0:42:04

CUDA中的图内存节点

在这里插入图片描述

文章目录

  • CUDA中的图内存节点
    • 1. 简介
    • 2. 支持的架构和版本
    • 3. API基础知识
      • 3.1. 图节点 APIs
      • 3.2. 流捕获
      • 3.3. 在分配图之外访问和释放图内存
      • 3.4. cudaGraphInstantiateFlagAutoFreeOnLaunch
    • 4. 优化内存复用
      • 4.1. 解决图中的重用问题
      • 4.2. 物理内存管理和共享
    • 5. 性能考虑
      • 5.1. First Launch / cudaGraphUpload
    • 6. Physical Memory Footprint
    • 7. Peer Access
      • 7.1. Peer Access with Graph Node APIs
      • 7.2. Peer Access with Stream Capture

1. 简介

图内存节点允许图创建和拥有内存分配功能。图内存节点具有 GPU 有序生命周期语义,它指示何时允许在设备上访问内存。这些 GPU 有序生命周期语义支持驱动程序管理的内存重用,并与流序分配 API cudaMallocAsynccudaFreeAsync 相匹配,这可能在创建图形时被捕获。

图分配在图的生命周期内具有固定的地址,包括重复的实例化和启动。这允许图中的其他操作直接引用内存,而无需更新图,即使 CUDA 更改了后备物理内存也是如此。在一个图中,其图有序生命周期不重叠的分配可以使用相同的底层物理内存。

CUDA 可以重用相同的物理内存进行跨多个图的分配,根据 GPU 有序生命周期语义对虚拟地址映射进行别名化。例如,当不同的图被启动到同一个流中时,CUDA 可以虚拟地为相同的物理内存取别名,以满足具有单图生命周期的分配的需求。

2. 支持的架构和版本

图内存节点需要支持 11.4 的 CUDA 驱动程序并支持 GPU 上的流序分配器。 以下代码段显示了如何检查给定设备上的支持。

int driverVersion = 0;
int deviceSupportsMemoryPools = 0;
int deviceSupportsMemoryNodes = 0;
cudaDriverGetVersion(&driverVersion);
if (driverVersion >= 11020) { // avoid invalid value error in cudaDeviceGetAttribute
    cudaDeviceGetAttribute(&deviceSupportsMemoryPools, cudaDevAttrMemoryPoolsSupported, device);
}
deviceSupportsMemoryNodes = (driverVersion >= 11040) && (deviceSupportsMemoryPools != 0);

在驱动程序版本检查中执行属性查询可避免 11.0 和 11.1 驱动程序上的无效值返回代码。 请注意,计算清理程序在检测到 CUDA 返回错误代码时会发出警告,并且在读取属性之前进行版本检查将避免这种情况。 图形内存节点仅在驱动程序版本 11.4 和更高版本上受支持。

3. API基础知识

图内存节点是表示内存分配或空闲操作的图节点。 简而言之,分配内存的节点称为分配节点。 同样,释放内存的节点称为空闲节点。 分配节点创建的分配称为图分配。 CUDA 在节点创建时为图分配分配虚拟地址。 虽然这些虚拟地址在分配节点的生命周期内是固定的,但分配内容在释放操作之后不会持久,并且可能被引用不同分配的访问覆盖。

每次图运行时,图分配都被视为重新创建。 图分配的生命周期与节点的生命周期不同,从 GPU 执行到达分配图节点时开始,并在发生以下情况之一时结束:

  • GPU 执行到达释放图节点
  • GPU 执行到达释放 cudaFreeAsync() 流调用
  • 立即释放对 cudaFree() 的调用

注意:图销毁不会自动释放任何实时图分配的内存,即使它结束了分配节点的生命周期。 随后必须在另一个图中或使用 cudaFreeAsync()/cudaFree() 释放分配。
就像其他图节点一样,图内存节点在图中按依赖边排序。 程序必须保证访问图内存的操作:

  • 在分配节点之后排序。
  • 在释放内存的操作之前排序

图分配生命周期根据 GPU 执行开始和结束(与 API 调用相反)。 GPU 排序是工作在 GPU 上运行的顺序,而不是工作队列或描述的顺序。 因此,图分配被认为是“GPU 有序”。

3.1. 图节点 APIs

可以使用内存节点创建 API、cudaGraphAddMemAllocNodecudaGraphAddMemFreeNode 显式创建图形内存节点。 cudaGraphAddMemAllocNode 分配的地址在传递的 CUDA_MEM_ALLOC_NODE_PARAMS 结构的 dptr 字段中返回给用户。 在分配图中使用图分配的所有操作必须在分配节点之后排序。 类似地,任何空闲节点都必须在图中所有分配的使用之后进行排序。 cudaGraphAddMemFreeNode 创建空闲节点。

在下图中,有一个带有分配和空闲节点的示例图。 内核节点 abc 在分配节点之后和空闲节点之前排序,以便内核可以访问分配。 内核节点 e 没有排在 alloc 节点之后,因此无法安全地访问内存。 内核节点 d 没有排在空闲节点之前,因此它不能安全地访问内存。

以下代码片段建立了该图中的图:

// Create the graph - it starts out empty
cudaGraphCreate(&graph, 0);

// parameters for a basic allocation
cudaMemAllocNodeParams params = {};
params.poolProps.allocType = cudaMemAllocationTypePinned;
params.poolProps.location.type = cudaMemLocationTypeDevice;
// specify device 0 as the resident device
params.poolProps.location.id = 0;
params.bytesize = size;

cudaGraphAddMemAllocNode(&allocNode, graph, NULL, 0, &params);
nodeParams->kernelParams[0] = params.dptr;
cudaGraphAddKernelNode(&a, graph, &allocNode, 1, &nodeParams);
cudaGraphAddKernelNode(&b, graph, &a, 1, &nodeParams);
cudaGraphAddKernelNode(&c, graph, &a, 1, &nodeParams);
cudaGraphNode_t dependencies[2];
// kernel nodes b and c are using the graph allocation, so the freeing node must depend on them.  Since the dependency of node b on node a establishes an indirect dependency, the free node does not need to explicitly depend on node a.
dependencies[0] = b;
dependencies[1] = c;
cudaGraphAddMemFreeNode(&freeNode, graph, dependencies, 2, params.dptr);
// free node does not depend on kernel node d, so it must not access the freed graph allocation.
cudaGraphAddKernelNode(&d, graph, &c, 1, &nodeParams);

// node e does not depend on the allocation node, so it must not access the allocation.  This would be true even if the freeNode depended on kernel node e.
cudaGraphAddKernelNode(&e, graph, NULL, 0, &nodeParams);

3.2. 流捕获

可以通过捕获相应的流序分配和免费调用 cudaMallocAsynccudaFreeAsync 来创建图形内存节点。 在这种情况下,捕获的分配 API 返回的虚拟地址可以被图中的其他操作使用。 由于流序的依赖关系将被捕获到图中,流序分配 API 的排序要求保证了图内存节点将根据捕获的流操作正确排序(对于正确编写的流代码)。

忽略内核节点 de,为清楚起见,以下代码片段显示了如何使用流捕获来创建上图中的图形:

cudaMallocAsync(&dptr, size, stream1);
kernel_A<<< ..., stream1 >>>(dptr, ...);

// Fork into stream2
cudaEventRecord(event1, stream1);
cudaStreamWaitEvent(stream2, event1);

kernel_B<<< ..., stream1 >>>(dptr, ...);
// event dependencies translated into graph dependencies, so the kernel node created by the capture of kernel C will depend on the allocation node created by capturing the cudaMallocAsync call. 
kernel_C<<< ..., stream2 >>>(dptr, ...);

// Join stream2 back to origin stream (stream1)
cudaEventRecord(event2, stream2);
cudaStreamWaitEvent(stream1, event2);

// Free depends on all work accessing the memory.
cudaFreeAsync(dptr, stream1);

// End capture in the origin stream
cudaStreamEndCapture(stream1, &graph);

3.3. 在分配图之外访问和释放图内存

图分配不必由分配图释放。当图不释放分配时,该分配会在图执行之后持续存在,并且可以通过后续 CUDA 操作访问。这些分配可以在另一个图中访问或直接通过流操作访问,只要访问操作在分配之后通过 CUDA 事件和其他流排序机制进行排序。随后可以通过定期调用 cudaFree、cudaFreeAsync 或通过启动具有相应空闲节点的另一个图,或随后启动分配图(如果它是使用 cudaGraphInstantiateFlagAutoFreeOnLaunch 标志实例化)来释放分配。在内存被释放后访问内存是非法的 - 必须在所有使用图依赖、CUDA 事件和其他流排序机制访问内存的操作之后对释放操作进行排序。

注意:因为图分配可能彼此共享底层物理内存,所以必须考虑与一致性和一致性相关的虚拟混叠支持规则。简单地说,空闲操作必须在完整的设备操作(例如,计算内核/ memcpy)完成后排序。具体来说,带外同步——例如,作为访问图形内存的计算内核的一部分,通过内存进行信号交换——不足以提供对图形内存的写操作和该图形内存的自由操作之间的排序保证。

以下代码片段演示了在分配图之外访问图分配,并通过以下方式正确建立顺序:使用单个流,使用流之间的事件,以及使用嵌入到分配和释放图中的事件。

使用单个流建立的排序:

void *dptr;
cudaGraphAddMemAllocNode(&allocNode, allocGraph, NULL, 0, &params);
dptr = params.dptr;

cudaGraphInstantiate(&allocGraphExec, allocGraph, NULL, NULL, 0);

cudaGraphLaunch(allocGraphExec, stream);
kernel<<< …, stream >>>(dptr, …);
cudaFreeAsync(dptr, stream);

通过记录和等待 CUDA 事件建立的排序:

void *dptr;

// Contents of allocating graph
cudaGraphAddMemAllocNode(&allocNode, allocGraph, NULL, 0, &params);
dptr = params.dptr;

// contents of consuming/freeing graph
nodeParams->kernelParams[0] = params.dptr;
cudaGraphAddKernelNode(&a, graph, NULL, 0, &nodeParams);
cudaGraphAddMemFreeNode(&freeNode, freeGraph, &a, 1, dptr);

cudaGraphInstantiate(&allocGraphExec, allocGraph, NULL, NULL, 0);
cudaGraphInstantiate(&freeGraphExec, freeGraph, NULL, NULL, 0);

cudaGraphLaunch(allocGraphExec, allocStream);

// establish the dependency of stream2 on the allocation node
// note: the dependency could also have been established with a stream synchronize operation
cudaEventRecord(allocEvent, allocStream)
cudaStreamWaitEvent(stream2, allocEvent);

kernel<<< …, stream2 >>> (dptr, …);

// establish the dependency between the stream 3 and the allocation use
cudaStreamRecordEvent(streamUseDoneEvent, stream2);
cudaStreamWaitEvent(stream3, streamUseDoneEvent);

// it is now safe to launch the freeing graph, which may also access the memory
cudaGraphLaunch(freeGraphExec, stream3);

使用图外部事件节点建立的排序:

void *dptr;
cudaEvent_t allocEvent; // event indicating when the allocation will be ready for use.
cudaEvent_t streamUseDoneEvent; // event indicating when the stream operations are done with the allocation.

// Contents of allocating graph with event record node
cudaGraphAddMemAllocNode(&allocNode, allocGraph, NULL, 0, &params);
dptr = params.dptr;
// note: this event record node depends on the alloc node
cudaGraphAddEventRecordNode(&recordNode, allocGraph, &allocNode, 1, allocEvent);
cudaGraphInstantiate(&allocGraphExec, allocGraph, NULL, NULL, 0);

// contents of consuming/freeing graph with event wait nodes
cudaGraphAddEventWaitNode(&streamUseDoneEventNode, waitAndFreeGraph, NULL, 0, streamUseDoneEvent);
cudaGraphAddEventWaitNode(&allocReadyEventNode, waitAndFreeGraph, NULL, 0, allocEvent);
nodeParams->kernelParams[0] = params.dptr;

// The allocReadyEventNode provides ordering with the alloc node for use in a consuming graph.
cudaGraphAddKernelNode(&kernelNode, waitAndFreeGraph, &allocReadyEventNode, 1, &nodeParams);

// The free node has to be ordered after both external and internal users.
// Thus the node must depend on both the kernelNode and the 
// streamUseDoneEventNode.
dependencies[0] = kernelNode;
dependencies[1] = streamUseDoneEventNode;
cudaGraphAddMemFreeNode(&freeNode, waitAndFreeGraph, &dependencies, 2, dptr);
cudaGraphInstantiate(&waitAndFreeGraphExec, waitAndFreeGraph, NULL, NULL, 0);

cudaGraphLaunch(allocGraphExec, allocStream);

// establish the dependency of stream2 on the event node satisfies the ordering requirement
cudaStreamWaitEvent(stream2, allocEvent);
kernel<<< …, stream2 >>> (dptr, …);
cudaStreamRecordEvent(streamUseDoneEvent, stream2);

// the event wait node in the waitAndFreeGraphExec establishes the dependency on the “readyForFreeEvent” that is needed to prevent the kernel running in stream two from accessing the allocation after the free node in execution order.
cudaGraphLaunch(waitAndFreeGraphExec, stream3);

3.4. cudaGraphInstantiateFlagAutoFreeOnLaunch

在正常情况下,如果图有未释放的内存分配,CUDA 将阻止重新启动图,因为同一地址的多个分配会泄漏内存。使用 cudaGraphInstantiateFlagAutoFreeOnLaunch 标志实例化图允许图在其仍有未释放的分配时重新启动。在这种情况下,启动会自动插入一个异步释放的未释放分配。

启动时自动对于单生产者多消费者算法很有用。在每次迭代中,生产者图创建多个分配,并且根据运行时条件,一组不同的消费者访问这些分配。这种类型的变量执行序列意味着消费者无法释放分配,因为后续消费者可能需要访问。启动时自动释放意味着启动循环不需要跟踪生产者的分配 - 相反,该信息与生产者的创建和销毁逻辑保持隔离。通常,启动时自动释放简化了算法,否则该算法需要在每次重新启动之前释放图所拥有的所有分配。
注意: cudaGraphInstantiateFlagAutoFreeOnLaunch 标志不会改变图销毁的行为。应用程序必须显式释放未释放的内存以避免内存泄漏,即使对于使用标志实例化的图也是如此。
以下代码展示了使用 cudaGraphInstantiateFlagAutoFreeOnLaunch 来简化单生产者/多消费者算法:

// Create producer graph which allocates memory and populates it with data
cudaStreamBeginCapture(cudaStreamPerThread, cudaStreamCaptureModeGlobal);
cudaMallocAsync(&data1, blocks * threads, cudaStreamPerThread);
cudaMallocAsync(&data2, blocks * threads, cudaStreamPerThread);
produce<<<blocks, threads, 0, cudaStreamPerThread>>>(data1, data2);
...
cudaStreamEndCapture(cudaStreamPerThread, &graph);
cudaGraphInstantiateWithFlags(&producer,
                              graph,
                              cudaGraphInstantiateFlagAutoFreeOnLaunch);
cudaGraphDestroy(graph);

// Create first consumer graph by capturing an asynchronous library call
cudaStreamBeginCapture(cudaStreamPerThread, cudaStreamCaptureModeGlobal);
consumerFromLibrary(data1, cudaStreamPerThread);
cudaStreamEndCapture(cudaStreamPerThread, &graph);
cudaGraphInstantiateWithFlags(&consumer1, graph, 0); //regular instantiation
cudaGraphDestroy(graph);

// Create second consumer graph
cudaStreamBeginCapture(cudaStreamPerThread, cudaStreamCaptureModeGlobal);
consume2<<<blocks, threads, 0, cudaStreamPerThread>>>(data2);
...
cudaStreamEndCapture(cudaStreamPerThread, &graph);
cudaGraphInstantiateWithFlags(&consumer2, graph, 0);
cudaGraphDestroy(graph);

// Launch in a loop
bool launchConsumer2 = false;
do {
    cudaGraphLaunch(producer, myStream);
    cudaGraphLaunch(consumer1, myStream);
    if (launchConsumer2) {
        cudaGraphLaunch(consumer2, myStream);
    }
} while (determineAction(&launchConsumer2));

cudaFreeAsync(data1, myStream);
cudaFreeAsync(data2, myStream);

cudaGraphExecDestroy(producer);
cudaGraphExecDestroy(consumer1);
cudaGraphExecDestroy(consumer2);

4. 优化内存复用

CUDA 以两种方式重用内存:

  • 图中的虚拟和物理内存重用基于虚拟地址分配,就像在流序分配器中一样。
  • 图之间的物理内存重用是通过虚拟别名完成的:不同的图可以将相同的物理内存映射到它们唯一的虚拟地址。

4.1. 解决图中的重用问题

CUDA 可以通过将相同的虚拟地址范围分配给生命周期不重叠的不同分配来重用图中的内存。 由于可以重用虚拟地址,因此不能保证指向具有不相交生命周期的不同分配的指针是唯一的。

下图显示了添加一个新的分配节点 (2),它可以重用依赖节点 (1) 释放的地址。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WyskpwCH-1675998508667)(new-alloc-node.png)]

下图显示了添加新的 alloc 节点(3)。 新的分配节点不依赖于空闲节点 (2),因此不能重用来自关联分配节点 (2) 的地址。 如果分配节点 (2) 使用由空闲节点 (1) 释放的地址,则新分配节点 3 将需要一个新地址。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FSu9HS4r-1675998508667)(adding-new-alloc-nodes.png)]

4.2. 物理内存管理和共享

CUDA 负责在按 GPU 顺序到达分配节点之前将物理内存映射到虚拟地址。作为内存占用和映射开销的优化,如果多个图不会同时运行,它们可能会使用相同的物理内存进行不同的分配,但是如果它们同时绑定到多个执行图,则物理页面不能被重用,或未释放的图形分配。

CUDA 可以在图形实例化、启动或执行期间随时更新物理内存映射。 CUDA 还可以在未来的图启动之间引入同步,以防止实时图分配引用相同的物理内存。对于任何 allocate-free-allocate 模式,如果程序在分配的生命周期之外访问指针,错误的访问可能会默默地读取或写入另一个分配拥有的实时数据(即使分配的虚拟地址是唯一的)。使用计算清理工具可以捕获此错误。

下图显示了在同一流中按顺序启动的图形。在此示例中,每个图都会释放它分配的所有内存。由于同一流中的图永远不会同时运行,CUDA 可以而且应该使用相同的物理内存来满足所有分配。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yqcLZiwv-1675998508667)(sequentially-launched-graphs.png)]

5. 性能考虑

当多个图启动到同一个流中时,CUDA 会尝试为它们分配相同的物理内存,因为这些图的执行不能重叠。 在启动之间保留图形的物理映射作为优化以避免重新映射的成本。 如果稍后启动其中一个图,使其执行可能与其他图重叠(例如,如果它启动到不同的流中),则 CUDA 必须执行一些重新映射,因为并发图需要不同的内存以避免数据损坏 .

一般来说,CUDA中图内存的重新映射很可能是由这些操作引起的

  • 更改启动图形的流
  • 图内存池上的修剪操作,显式释放未使用的内存(在物理内存占用中讨论)
  • 当另一个图的未释放分配映射到同一内存时重新启动一个图将导致在重新启动之前重新映射内存

重新映射必须按执行顺序发生,但在该图的任何先前执行完成之后(否则可能会取消映射仍在使用的内存)。 由于这种排序依赖性,以及映射操作是操作系统调用,映射操作可能相对昂贵。 应用程序可以通过将包含分配内存节点的图一致地启动到同一流中来避免这种成本。

5.1. First Launch / cudaGraphUpload

在图实例化期间无法分配或映射物理内存,因为图将在其中执行的流是未知的。 映射是在图形启动期间完成的。 调用 cudaGraphUpload 可以通过立即执行该图的所有映射并将该图与上传流相关联,将分配成本与启动分开。 如果图随后启动到同一流中,它将启动而无需任何额外的重新映射。

使用不同的流进行图上传和图启动的行为类似于切换流,可能会导致重新映射操作。 此外,允许无关的内存池管理从空闲流中提取内存,这可能会抵消上传的影响。

6. Physical Memory Footprint

异步分配的池管理行为意味着销毁包含内存节点的图(即使它们的分配是空闲的)不会立即将物理内存返回给操作系统以供其他进程使用。要显式将内存释放回操作系统,应用程序应使用 cudaDeviceGraphMemTrim API。

cudaDeviceGraphMemTrim 将取消映射并释放由图形内存节点保留的未主动使用的任何物理内存。尚未释放的分配和计划或运行的图被认为正在积极使用物理内存,不会受到影响。使用修剪 API 将使物理内存可用于其他分配 API 和其他应用程序或进程,但会导致 CUDA 在下次启动修剪图时重新分配和重新映射内存。请注意,cudaDeviceGraphMemTrim 在与 cudaMemPoolTrimTo() 不同的池上运行。图形内存池不会暴露给流序内存分配器。 CUDA 允许应用程序通过 cudaDeviceGetGraphMemAttribute API 查询其图形内存占用量。查询属性 cudaGraphMemAttrReservedMemCurrent 返回驱动程序为当前进程中的图形分配保留的物理内存量。查询 cudaGraphMemAttrUsedMemCurrent 返回至少一个图当前映射的物理内存量。这些属性中的任何一个都可用于跟踪 CUDA 何时为分配图而获取新的物理内存。这两个属性对于检查共享机制节省了多少内存都很有用。

7. Peer Access

图分配可以配置为从多个 GPU 访问,在这种情况下,CUDA 将根据需要将分配映射到对等 GPU。 CUDA 允许需要不同映射的图分配重用相同的虚拟地址。 发生这种情况时,地址范围将映射到不同分配所需的所有 GPU。 这意味着分配有时可能允许比其创建期间请求的更多对等访问; 然而,依赖这些额外的映射仍然是一个错误。

7.1. Peer Access with Graph Node APIs

cudaGraphAddMemAllocNode API 接受节点参数结构的 accessDescs 数组字段中的映射请求。 poolProps.location 嵌入式结构指定分配的常驻设备。 假设需要来自分配 GPU 的访问,因此应用程序不需要在 accessDescs 数组中为常驻设备指定条目。

cudaMemAllocNodeParams params = {};
params.poolProps.allocType = cudaMemAllocationTypePinned;
params.poolProps.location.type = cudaMemLocationTypeDevice;
// specify device 1 as the resident device
params.poolProps.location.id = 1;
params.bytesize = size;

// allocate an allocation resident on device 1 accessible from device 1
cudaGraphAddMemAllocNode(&allocNode, graph, NULL, 0, &params);

accessDescs[2];
// boilerplate for the access descs (only ReadWrite and Device access supported by the add node api)
accessDescs[0].flags = cudaMemAccessFlagsProtReadWrite;
accessDescs[0].location.type = cudaMemLocationTypeDevice;
accessDescs[1].flags = cudaMemAccessFlagsProtReadWrite;
accessDescs[1].location.type = cudaMemLocationTypeDevice;

// access being requested for device 0 & 2.  Device 1 access requirement left implicit.
accessDescs[0].location.id = 0;
accessDescs[1].location.id = 2;

// access request array has 2 entries.
params.accessDescCount = 2;
params.accessDescs = accessDescs;

// allocate an allocation resident on device 1 accessible from devices 0, 1 and 2. (0 & 2 from the descriptors, 1 from it being the resident device).
cudaGraphAddMemAllocNode(&allocNode, graph, NULL, 0, &params);

7.2. Peer Access with Stream Capture

对于流捕获,分配节点在捕获时记录分配池的对等可访问性。 在捕获 cudaMallocFromPoolAsync 调用后更改分配池的对等可访问性不会影响图将为分配进行的映射。

// boilerplate for the access descs (only ReadWrite and Device access supported by the add node api)
accessDesc.flags = cudaMemAccessFlagsProtReadWrite;
accessDesc.location.type = cudaMemLocationTypeDevice;
accessDesc.location.id = 1;

// let memPool be resident and accessible on device 0

cudaStreamBeginCapture(stream);
cudaMallocAsync(&dptr1, size, memPool, stream);
cudaStreamEndCapture(stream, &graph1);

cudaMemPoolSetAccess(memPool, &accessDesc, 1);

cudaStreamBeginCapture(stream);
cudaMallocAsync(&dptr2, size, memPool, stream);
cudaStreamEndCapture(stream, &graph2);

//The graph node allocating dptr1 would only have the device 0 accessibility even though memPool now has device 1 accessibility.
//The graph node allocating dptr2 will have device 0 and device 1 accessibility, since that was the pool accessibility at the time of the cudaMallocAsync call.

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

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

相关文章

C. Least Prefix Sum codeforces每日一题

&#x1f680;前言 &#x1f680; 大家好啊&#xff0c;这里是幸麟 &#x1f9e9; 一名普通的大学牲&#xff0c;最近在学习算法 &#x1f9e9;每日一题的话难度的话是根据博主水平来找的 &#x1f9e9;所以可能难度比较低&#xff0c;以后会慢慢提高难度的 &#x1f9e9;此题标…

ImportError: /lib64/libm.so.6: version `GLIBC_2.23‘ not found问题解决方法

1.环境&#xff1a;Centos7&#xff0c;GCC version 9.1.0&#xff0c;python3.7&#xff0c;TensorFlow1.14.0.因为/usr/lib64/libstdc.so.6: version CXXABI_1.3.8 not found问题&#xff0c;我将GCC版本升级到了9.1.0&#xff0c;但是运行TensorFlow的时候出现了ImportError…

网页CAD开发快速入门

演示说明 提示:目前提供两种在网页中浏览编辑CAD图纸方案&#xff0c;详细说明见&#xff1a;MxDraw帮助 网页中打开CAD最简步骤&#xff1a; 第一步: 安装插件运行环境&#xff0c;下载安装(可能需要退杀毒软件)&#xff1a;https://demo.mxdraw3d.com:3562/MxDrawx86Setup…

Python语言零基础入门教程(十一)

Python 列表(List) 序列是Python中最基本的数据结构。序列中的每个元素都分配一个数字 - 它的位置&#xff0c;或索引&#xff0c;第一个索引是0&#xff0c;第二个索引是1&#xff0c;依此类推。 Python有6个序列的内置类型&#xff0c;但最常见的是列表和元组。 序列都可以…

springboot 关闭所有日志,包括起始springboot图标和运行输入日志

关闭所有日志分2部分: 1 关闭运行输出日志 , 2 关闭springboot启动图标 1 关闭运行输出日志 (这里没有使用日志框架设置日志) 在 application.properties 中 添加 logging.level.你自己项目的包名off例如&#xff1a;logging.level.com.example.licenseoff 注&#xff1a;of…

leetcode 2306. Naming a Company(命名公司)

字符串ideas中有一系列的名字&#xff0c;用这些名字给公司命名&#xff0c;命名规则如下&#xff1a; 在ideas中选出2个不同的单词&#xff0c;交换它们的首字母&#xff0c; 交换首字母后的两个单词如果不和ideas中的任一单词相同&#xff0c;那么就可以用它们命名&#xff0…

Vue - 驼峰和短横线分割命名注意事项

Vue - 驼峰和短横线分割命名注意事项一. 驼峰和短横线分割命名注意事项1.1 组件注册命名1.2 父子组件数据传递时命名1.3 父子组件函数传递一. 驼峰和短横线分割命名注意事项 我们一般定义组件的方式有两种&#xff1a; 短横线分隔命名&#xff1a;kebab-case。首字母大写命名…

【实际开发17】- 静态测试

静态测试技术&#xff1a;不运行被测试程序&#xff0c;对代码通过检查、阅读进行分析。 目录 1. 静态测试 1. 静态测试三步曲 : 走查 / 审查 / 评审 2. 编码的标准和规范 3. 代码评审 1. 代码走查 ( Walk Through ) 2. 正式会议审查 ( Inspection ) 3. 评审 ( Review )…

QThread(创建线程)

在Qt 中QThread 类提供了于平台无关的线程&#xff0c;一个QThread代表一个在应用程序中可以独立控制的线程&#xff0c;可以和进程中的其他线程分享数据。 QThread 对象管理程序中的一个控制线程。QThreads 在 run&#xff08;&#xff09; 中开始执行。默认情况下&#xff0…

C语言进阶——自定义类型:结构体

&#x1f307;个人主页&#xff1a;_麦麦_ &#x1f4da;今日名言&#xff1a;生活不可能像你想象的那么好&#xff0c;也不会像你想象的那么糟。——莫泊桑《羊脂球》 目录 一、前言 二、正文 1结构体 1.1结构体的基础知识 1.2结构的声明 1.3特殊的声明 1.4结构体变量的…

若依框架---数据转树状层级

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是小童&#xff0c;Java开发工程师&#xff0c;CSDN博客博主&#xff0c;Java领域新星创作者 &#x1f4d5;系列专栏&#xff1a;前端、Java、Java中间件大全、微信小程序、微信支付、若依框架、Spring全家桶 &#x1f4…

Node=>Express路由 学习2

1.概念 Express路由指的是客户端的请求与服务器处理函数之间的映射关系 Express路由由三部分组成 请求类型 请求URL地址 处理函数 app.METHOD ( PATH , HANDLER )根据定义的先后顺序进行匹配 请求类型和请求的URl同时匹配成功才会调用相应的处理函数 简单用法 2.模块化路由 为了…

《里奥哈酒友记》 | 里奥哈的历史—品鉴瑞格尔侯爵葡萄酒

2022年末&#xff0c;里奥哈大使组合怪怪和思羽完成了里奥哈线上活动两大“壮举”&#xff0c;10期《里奥哈酒友记》系列视频和40集《美酒之乡——里奥哈》有声专辑&#xff0c;吸引了许多葡萄酒资深爱好者的目光&#xff0c;也成功地让更多的人了解到里奥哈。由里奥哈推广大使…

粘包拆包问题

什么是粘包/拆包 TCP是个“流”式的协议&#xff0c;会根据TCP缓冲区的实际情况进行包的划分。在业务层面认为一个完整的包&#xff0c;可能会被TCP拆分成多个小包进行发送&#xff0c;也可能把多个小的包封装成一个大的数据包进行发送 粘包拆包问题解决方法 由于TCP协议底层…

电脑分盘怎么分?分盘详细教程来了,图文教学

电脑作为小伙伴日常生活使用的工具&#xff0c;很多事情都需要使用电脑来进行处理。虽然小伙伴使用电脑比较多&#xff0c;但是还是有不少的小伙伴不知道电脑分盘怎么分&#xff1f;其实电脑分盘很简单&#xff0c;下面小编就以图文教学的方式&#xff0c;详细的向小伙伴介绍电…

Python:迷宫

题目描述 本题为填空题&#xff0c;只需要算出结果后&#xff0c;在代码中使用输出语句将所填结果输出即可。 X 星球的一处迷宫游乐场建在某个小山坡上。它是由 1010 相互连通的小房间组成的。 房间的地板上写着一个很大的字母。我们假设玩家是面朝上坡的方向站立&#xff0…

【Linux系统编程】01:命令行解析

命令行解析 1.getopt调用介绍 头文件&#xff1a;<unistd.h> 原型&#xff1a;int getopt(int argc, char * const argv[], const char *optstring); argc、argv&#xff1a;由main函数的参数直接传递而来optstring&#xff1a;一个包含所有合法可选字符组成的字符串 …

超高精度PID调节器的特殊功能(3)——变送输出(转发)功能及其应用

摘要&#xff1a;变送输出是高级PID控制器的一项重要扩展功能&#xff0c;可用于多区控制、串级控制、比值控制和差值控制以及数据采集及记录。为展示变送输出功能的强大作用&#xff0c;本文主要针对超高精度VPC 2021系列PID控制器&#xff0c;介绍了变送输出的具体功能、参数…

【FPGA】Verilog:组合电路 | 3—8译码器 | 编码器 | 74LS148

前言&#xff1a;本章内容主要是演示Vivado下利用Verilog语言进行电路设计、仿真、综合和下载 示例&#xff1a;编码/译码器的应用 ​ 功能特性&#xff1a; 采用 Xilinx Artix-7 XC7A35T芯片 配置方式&#xff1a;USB-JTAG/SPI Flash 高达100MHz 的内部时钟速度 存储器&…

部门新来了个软件测试工程师,看着一副毛头小子的样儿,哪想到是新一代卷王...

内卷&#xff0c;是现在热度非常高的一个词汇&#xff0c;随着热度不断攀升&#xff0c;隐隐到了“万物皆可卷”的程度。 在程序员职场上&#xff0c;什么样的人最让人反感呢? 是技术不好的人吗?并不是。技术不好的同事&#xff0c;我们可以帮他。 是技术太强的人吗?也不…