AI推理计算框架中的内存优化

news2024/9/25 9:27:47

背景

内存管理是AI计算中非常重要的一部分。我们希望模型计算时占用内存尽可能小,这样我们训练或推理时就可以用更大的batch size使其尽快收敛,或者提高吞吐率。又或者让我们可以使用参数更多、或更复杂的模型从而达到更好的准确率。由于现代深度学习模型大多在GPU上运行,而GPU的显存相比CPU小得多,因此我们这里主要关注的是GPU memory。

首先看下我们需要重点关注哪些GPU memory。对于模型在计算中需要用到的GPU memory,论文《Estimating GPU Memory Consumption of Deep Learning Models》做了比较具体的总结。对推理计算而言,主要有这么几类:

  • 权重参数(Weight Parameter):这个不用多说,模型中的参数。
  • 中间张量(Intermediate Tensor):网络中每层的输出与输入张量。
  • 其它:计算中需要的临时内存(如kernel中使用的一些memory,cuDNN的workspace),还有一些常驻的内存(如CUDA context)。

其中第三类本身占的空间不大,也比较难优化。第一类减少权重内存占用的话可以通过一些模型压缩方法,如量化,剪枝。之前写过一些相关文章如《闲话模型压缩之量化(Quantization)篇》和《闲话模型压缩之网络剪枝(Network Pruning)篇》,有兴趣可以参考。相比之下,第二类,即中间张量的优化空间更大,因此很多业界的工作也是针对它来优化。优化的思路有很多,比如:

  • 内存重用(Memory reuse):由于有些中间张量的生命周期间互不重叠,因此可以reuse。MegEngine, IREE, TensorRT等框架都做了memory usage相关的优化。
  • 重计算(Recomputation):该技术主要用于训练中。它将模型中的一些节点作为checkpoint,其它节点的输出可丢弃,当在计算梯度时需要时通过最近的checkpoint重新计算生成。因此被称为checkpointing技术。该问题也被称为tensor rematerialization优化问题。相关论文如适用于静态网络的offline方法《Training Deep Nets with Sublinear Memory Cost》,适用于动态网络的online方法《Dynamic Tensor Rematerialization》,建模为MILP进行求解的《Checkmate: Breaking the Memory Wall with Optimal Tensor Rematerialization》等。
  • 交换(Swap):基本思想是将显存中的数据交换到CPU上,相当于把GPU memory当成CPU memory的cache。一些塞不进GPU显存的层,如DLRM模型的embedding层可能会用到这种技术。相关的论文如《Supporting Massive DLRM Inference Through Software Defined Memory》,《vDNN: Virtualized Deep Neural Networks for Scalable, Memory-Efficient Neural Network Design》等。
  • 压缩(Compression):将数据进行压缩,如《Gist: Efficient Data Encoding for Deep Neural Network Training》,根据特定层输出特点对层的输出,即feature map数据进行编码压缩。

后面几种都会一定程度上牺牲性能,这里我们主要关注第一种。它在对延迟关注的推理场景用得尤其多一些。比如TensorFlow Lite利用该技术可以显著减少内存占用(详见https://blog.tensorflow.org/2020/10/optimizing-tensorflow-lite-runtime.html)。

对于网络前面层的输出,到计算后面的层时可能已经不再使用了。换句话说,对于中间层的输出张量,它们的生命周期可能是没有重叠的,对于它们便可以进行重用。下面是最简单情况下的示意图:
在这里插入图片描述
其中Task 1写数据到Tensor 1交给Task 2,Task 2处理后将结果写入Tensor 2,交给Task 3。因为Tensor 1与Tensor 2生命周期并不重叠,所以它们可以重用同一个Tensor。典型的如神经网络中的一些element wise操作。

但实际中的情况远没有这么简单。网络中不总是线性结构,另外张量的大小可能各不相同,给重用带来困难。要得到其最优的分配策略,是一个NP-complete问题。因此实际当中,我们可以倾向于一些heuristic方法,这样可以在合理时间内得到一个近似最优解。

那如何优化呢?在不少地方,如TensorRT会提到基于register allocation的思想。任何一本编译器的教材中都会介绍register allocation,在此不展开。大体会先通过liveness analysis得到变量的live range,然后根据它生成inference graph,转为着色问题来解。业界也有采用这种方法的做法,如《Memory Allocation for Neural Networks using Graph Coloring》。但是,memory planning与register allocation所面临的问题还是有所区别的,比如:

  • 大小不确定:Register的大小基本是相同,或者说是差不多的,而memory的大小可能差异很大,重用一块过大或过小的memory会产生问题。
  • 拷贝成本高:Register拷贝一下还好,但memory拷贝开销比较大,尤其是大块的memory。因此理想情况下我们希望不要拷贝。
  • Fallback机制:Register实在分配不了会产生spill,即放到memory中。虽然memory要不不够理论上也能往更下一层存储器上搬,业界也有这方面的研究,但很多情况下因为时延等原因不会这么做。

另外,从静态/动态角度,内存的管理方式大体有动态分配与静态规划两种:

  • 动态分配(Dynamic Memory Allocation):内存分配在运行时进行。由于从系统中分配的开销较大,通常维护一个memory pool。需要时从中分配,不再需要时放回到pool。如TensorFlow中的BFC allocator。
  • 静态规划(Static Memory Planning):内存分配在运行前进行,常见于基于编译器的计算框架。通过规划进一步减少内存使用,减少OOM带来的不确定性,同时最少化运行时内存管理的开销。如MXNet与MegEngine/MegCC中的static memory planning。

光看概念有些抽象,下面就以一个实例 - TensorFlow Lite(TF Lite)中的内存优化来看看具体的实现。其实在论文《Efficient Memory Management for Deep Neural Net Inference》与《On-Device Neural Net Inference with Mobile GPUs》中对其原理已经介绍得比较清楚了。下面主要是结合代码理解下它的实现。

代码走读

基础数据结构

先来看几个关键数据结构。它们定义在types.h文件中。结构体TensorUsageRecord即论文中提到的Tensor usage record,用于记录张量的使用记录。它主要包含三个信息:tensor size, 以及第一个与最后一个使用它的task。代表这两个task的成员first_tasklast_task即它的生命周期。

using UsageGraph = std::vector<std::vector<size_t>>;

template <typename TensorSizeT>
struct TensorUsageRecord {
  TensorSizeT tensor_size;
  TaskId first_task;
  TaskId last_task;
  ...
};

注意它是个模板类,有针对size_tuint2uint3BHWC的特化(实现在memory_management.c文件)。

结构体ObjectsAssignmentOffsetsAssignment都用于存放分配结果。

// Information about assignment of tensors to shared objects
template <typename TensorSizeT>
struct ObjectsAssignment {
  // shared_object_ids_[i] is ID of shared object, that tensor i will be using.
  std::vector<size_t> object_ids;
  // shared_object_sizes_[i] is a size of shared object with ID equal to i.
  std::vector<TensorSizeT> object_sizes;
};

// Information about assignment of tensors to offsets for the case, when all of
// them are going to be allocated in one continuous memory block.
struct OffsetsAssignment {
  std::vector<size_t> offsets;
  size_t total_size;
};

它们对应后面会提到的两种分配方式。前者用于shared object(指可以用于多个tensor的内存块)分配,后者用于从大块连续内存中分配子内存区域。

为了解它的使用,可以参考它的测试用例memory_management_test.cc。比较典型的有几个case:OneRecordChainRecordsComplexRecords,分别对于只有一个节点,链式(即线性)计算图,和复杂计算图。
在这里插入图片描述

ChainRecords这个case为例:

TEST(Model, ChainRecords) {                                                          
  std::vector<TensorUsageRecord<size_t>> usage_records{                              
      {/*size=*/16, /*first=*/0, /*last=*/1},                                        
      {/*size=*/8, /*first=*/1, /*last=*/2},                                         
      {/*size=*/64, /*first=*/2, /*last=*/3},                                        
      {/*size=*/32, /*first=*/3, /*last=*/4},                                        
      {/*size=*/8, /*first=*/4, /*last=*/5},                                         
  };                                                                                 
                                                                                     
  ObjectsAssignment<size_t> assignment;                                              
  ASSERT_TRUE(                                                                       
      AssignObjectsToTensors(usage_records, MemoryStrategy::NAIVE, &assignment)      
          .ok());                                                                    
  EXPECT_THAT(assignment.object_ids, ElementsAre(0, 1, 2, 3, 4));                    
  EXPECT_THAT(assignment.object_sizes, ElementsAre(16, 8, 64, 32, 8));    
  ...

可以看到,其中最核心的是AssignObjectsToTensors()函数。该函数基于由TensorUsageRecord数组表示的张量使用记录(按拓扑序排列),根据指定的分配策略(由MemoryStrategy表示),计算得到分配结果(由ObjectsAssignment对象表示)。

Object分配方式

注意AssignObjectsToTensors()是个模板函数,根据TensorUsageRecord的类型不同有多种实现。以最简单的TensorUsageRecord<size>的情况(即tensor的大小以一个size_t类型表示)为例,相关代码如下:

template <>
absl::Status AssignObjectsToTensors(
    const std::vector<TensorUsageRecord<size_t>>& usage_records,
    MemoryStrategy strategy, ObjectsAssignment<size_t>* assignment,
    const UsageGraph* reallocation_graph) {
  switch (strategy) {
    case MemoryStrategy::NAIVE:
      return NaiveAssignment(usage_records, assignment);
    case MemoryStrategy::EQUALITY:
      return EqualityAssignmentWithHash(usage_records, assignment);
    case MemoryStrategy::GREEDY_IN_ORDER:
      return GreedyInOrderAssignment(usage_records, assignment,
                                     reallocation_graph);
    case MemoryStrategy::GREEDY_BY_BREADTH:
      return GreedyByBreadthAssignment(usage_records, assignment);
    case MemoryStrategy::GREEDY_BY_SIZE:
      return GreedyBySizeDistPriorityAssignment(usage_records, assignment);
    case MemoryStrategy::GREEDY_BEST:
      return BestGreedy(usage_records, assignment);
    case MemoryStrategy::MINCOSTFLOW:
      return MinCostFlowAssignment(usage_records, assignment);
    default:
      return absl::InternalError(
          "MemoryStrategy is not supported with current tensor size type.");
  }
  return absl::OkStatus();
}

可以看到,它的主体部分就是根据指定策略调用相应的分配算法。这几种策略分别是:

NATIVE

NaiveAssignment函数将每个张量分配独立的memory object。实现在native_assignment.h文件中。该算法为每个张量分配一个新的shared object。这是最简单,也是最浪费内存的做法。代码逻辑比较好理解,不多说了。

EQUALITY

EqualityAssignmentWithHash()函数实现于equality_assignment.h文件中。它适用于TensorSizeT为hashable type的情况(unhashable type的情况使用EqualityAssignment()函数)。

该算法维护两个关键数据结构:一个是pool,它是一个map。其键值为size,值为目前free(即空闲)的且size与键值相同的shared objects。另一个是优先队列objects_in_use,它保存目前在使用的share objects,并按size排序。整个过程遍历所有的tensor usage record,对于每个tensor usage record,先将队列objects_in_use中相对于当前tensor已不再使用的shared object弹出,放入pool。然后在pool中找有没有与当前tensor的size匹配的shared object,如有就重用,没有的话就新创建一个shared object。过程示意图如下:
在这里插入图片描述
注意这里只是做memory planning,所以不用真正做内存分配。

GREEDY_IN_ORDER

函数GreedyInOrderAssignment()实现在greedy_in_order_assignment.h文件中。它维护与前一算法中相似的两个数据结构:一个是存放free shared objects的pool,以object size排序。另一个是存放正在使用的shared object的优先队列objects_in_use,以last_task排序。该算法主体部分遍历tensor usage records。在第一步中,首先看哪些object不再使用,将它们放入pool。这一步与前面算法一样。

然后尝试将pool中的shared object分配给当前tensor。前面是需要严格匹配才能重用,这里放宽了一些,允许在size不严格一致时也能重用。这里在从pool中找可用的shared object时,不是用的find()函数,而是用的是二分查找lower_bound(),即查找不小于当前tensor的size的第一个元素。这样做保证找到的shared object(如有)能容纳当前tensor,同时又使浪费的空间尽可能小。然后尝试检查前一个元素,其shared object size与tensor size的差值是否比前面找到的元素更小。如果是就选它了。这样使得对shared object的size改变尽可能得小就能满足当前tensor的需求。比如pool中有size分别为1, 3, 6的shared object,而当前tensor为5。这种情况下就会找到size为3这个shared object,因为它与5的差值是最小的。可以看到,它每一步尽可能找与当前tensor的size尽可能接近的shared object进行重用,体现了贪心的思想。

当前面的步骤中找到合适的shared object,就把它从pool中拿走,将之分配给当前tensor,分配信息写入assignment。同时将该信息记录在objects_in_use中。如果shared object的size小于tensor,会增大shared object的size。如果没有找到合适的shared object,则创建新的shared object。

GREEDY_BY_BREADTH

函数GreedyByBreadthAssignment()实现在greedy_by_breadth_assignment.cc文件中。与前面的贪心算法类似,主要区别在于它优先考虑breadth大的task。Breadth表示该task执行时所有tensor的size之和,记录于TaskProfile对象中。TaskProfile是一个vector,元素表示task执行时还在使用的张量,以size降序排序。obj_schedulesSharedObjectSchedule的vector。SharedObjectSchedule记录了shared object对应的所有tensor usage record。

// Set of usage records for all tensors assigned to the shared object, ordered
// by first_task.
using SharedObjectSchedule = std::set<TensorUsageRecord<size_t>>;               
          
struct TaskBreadthWithId {
  size_t breadth;
  TaskId task_id;
              
  TaskBreadthWithId(size_t breadth, size_t task_id)                             
      : breadth(breadth), task_id(task_id) {}                                   
            
  // Default order of TaskBreadthWithId is increasing order of their breadth.   
  bool operator<(const TaskBreadthWithId& other) const {
    return breadth < other.breadth;
  }         
};        

算法首先调用CalculateTaskProfiles()函数计算出所有task的TaskProfile,它包含了task的breadth信息。然后以breadth非递增顺序遍历所有task。对于每个task,遍历其所有的tensor,为它们分配shared object。分配过程中考虑obj_schedules中的shared object是否可重用。遍历其中元素,如果不合适就跳过,否则用lower_bound()函数找到其中不小于当前所需size的第一个shared object。如果找到的话检查一下,如果合法,就将该shared object作为最优候选。如果没找到就创建新的shared object。

GREEDY_BY_SIZE

函数GreedyBySizeDistPriorityAssignment()实现在greedy_by_size_assignment.cc文件中。它的流程大体与前一种相似,区别在于优先考虑哪些tensor时所用的策略。该方法使用了一种更加复杂的heuristic。核心数据结构是SizeDistPriorityInfo

struct SizeDistPriorityInfo {
  // - Tensor with leftmost position in positional maximums vector has higher
  // priority;
  // - If two tensors have equal position, the one, that has usage interval with
  // smallest positive distance (best_dist) to some of already assigned tensors,
  // has higher priority;
  // - If two tensors have equal position and best_dist, the one with greater
  // tensor_size has higher priority.
  bool operator>(const SizeDistPriorityInfo& other) const {
    return position < other.position ||
           (position == other.position &&
            (best_dist < other.best_dist || (best_dist == other.best_dist &&
                                             tensor_size > other.tensor_size)));
  }

  // Recalculate best distance and best object, based on precalculated distances
  // in vector dist.
  void RecalcBestDist() {
    best_dist = kNotAssigned;
    for (size_t obj_id = 0; obj_id < dist.size(); ++obj_id) {
      if (dist[obj_id] < best_dist) {
        best_dist = dist[obj_id];
        best_object = obj_id;
      }
    }
  }

  size_t position;
  size_t tensor_size;
  std::vector<size_t> dist;
  size_t best_dist;
  size_t best_object;
  size_t tensor_usage_id;
};

根据上面的注释,在positional maximum vector中位置越靠左的tensor优先级越高。如果在该vector中位置一样,则与已分配的tensor的positive distance小的优先级更高。如果前两个标准还决不出高下,那size较大的tensor优先级更高。这个优先级考虑了tensor的size,也考虑了与时序上的局部性。

主流程中首先调用CalculatePositionalMaximums()函数计算positional maximums vector。即每个shared object size的lower bound的数组。然后根据这个信息填好SizeDistPriorityInfo数组。接下来分两层循环。它会以SizeDistPriority递增顺序遍历所有tensor。即对于每个tensor,按前面计算得到的优先级信息找到优先级最高的tensor,以及相应的object。如果找到就按之进行分配,没找到就创建新的shared object。分配完成后,SizeDistPriority信息会产生变化,因此需要进行相应的更新。

GREEDY_BEST

函数BestGreedy()实现在memory_management.cc文件中。它结合了前两种贪心算法。结合方式比较直观,即把GREEDY_BY_SIZEGREEDY_BY_BREADTH都跑下,看哪个的总用量少(即更优)就选哪个。

  RETURN_IF_ERROR(
      GreedyBySizeDistPriorityAssignment(usage_records, assignment));
  ObjectsAssignment<size_t> assignment_by_breadth;
  if (GreedyByBreadthAssignment(usage_records, &assignment_by_breadth).ok() &&
      TotalSize(assignment_by_breadth) < TotalSize(*assignment)) {
    std::swap(*assignment, assignment_by_breadth);
  }

MINCOSTFLOW

函数MinCostFlowAssignment()实现在min_cost_flow_assignment.cc文件中。
它使用Minimum-cost flow matching algorithm。它将问题建模为一个minimum-cost flow problem(MCFP)。对于原问题,它创建一个auxiliary flow graph,对于每个intermediate tensor在图中插入两个对应节点,另外创建两个特殊节点-source与sink。然后按一定规则向图中添加有向边(具体可参见论文《On-Devie Neural Net Inference with Mobile GPUs》)。创建完后,使用Shortest Path Faster Algorithm(SPFA)求解。

TF Lite中虽然提供了多种机制,但至于哪种好不一定,所以实际使用当中可能要都试一下。

Offset分配方式

除了这种以shared object分配的模式外,TF Lite还支持一种在一整块内存块分配的模式。两种模式区别示意图:
在这里插入图片描述
代码中检测如果支持sub buffer方式的话,会试图以memory中的切块(以offset表示)进行分配。一般使用在先分配一大块连续memory,然后在里边“切”小块的情况。这种情况下,会调用AssignOffsetsToTensors()函数,继而调用GreedyBySizeAssignment()函数。这是论文中提到的Offet Calculation方法。它可以看作是二维的strip packing problem。在主逻辑函数GreedyBySizeAssignment()中,需要注意的是两个比较关键的数据结构:ordered_records是按size排序后的TensorUsageRecordordered_allocs是已经分配好memory的tensor,按offset排序。主要逻辑包含两层循环。外循环按序遍历TensorUsageRecord,然后内循环中遍历已分配的内存块。如果生命周期不重叠,则会考察它前面空出的区域是否能容纳下当前的tensor。如果能容纳且浪费的区域最小,则将当前tensor塞进去。找一圈都找不到的话就在最右边新分配一段。

框架对接

看了内存分配的实现,再看下它是如何结合进框架,用于模型的计算的。我们知道TF Lite会将计算放到相应的后端,如Hexagon,OpenGL, OpenCL, Metal等。这些后端实现放在delegates目录下。以GPU的OpenCL后端为例,相应的实现主要位于tensorflow/tensorflow/lite/delegates/gpu/cl/inference_context.cc文件中。函数InitFromGpuModel()调用AllocateMemory()函数,函数AllocateMemory继而调用AllocateBufferBasedTensors()函数或者AllocateStrongShapesTensors函数。前者用于buffer based的tensor(参见IsBufferBased()函数),比如clCreateBuffer()分配的内存。其它的则用AllocateStrongShapesTensors()函数进行处理。函数AllocateStrongShapesTensors()用于那些具有特定layout的tensor(参考delegates/gpu/common/shape.h),调用AssignObjectsToTensors()函数时传入的分配策略是MemoryStrategy::EQUALIT

下面看下AllocateBufferBasedTensors()函数。其中核心函数GetBufferAsignment()负责buffer的分配。它先调用GetUsages()函数得到网络中张量的使用信息,然后基于它构成TensorUsageRecord数组。有了这些信息,就可以调用前面提到的AssignObjectsToTensors()函数进行内存对象的分配了。默认用的分配策略是MemoryStrategy::GREEDY_BEST

另外如果支持sub buffer的话,还会调用AssignOffsetsToTensors()进行分配。最后判断与前面以buffer为单位(AssignObjectsToTensors()函数)的分配方式得到的结果哪种好,即使用的memory少,从而选择更优的一种。

最后总结一下整个调用流程作为收尾吧:
在这里插入图片描述

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

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

相关文章

【MySQL数据库】主从复制原理和应用

主从复制和读写分离1. 主从复制的原理2. 主从复制的环境配置2.1 准备好数据库服务器2.2 配置master2.3 配置slave2.4 测试3. 主从复制的应用——读写分离3.1 读写分离的背景3.2 Sharding-JDBC介绍3.3 Sharding-JDBC使用步骤1. 主从复制的原理 MySQL主从复制是一个异步的过程&a…

微服务 RocketMQ-延时消息 消息过滤 管控台搜索问题

~~微服务 RocketMQ-延时消息 消息过滤 管控台搜索问题~~ RocketMQ-延时消息实现延时消息RocketMQ-消息过滤Tag标签过滤SQL标签过滤管控台搜索问题RocketMQ-延时消息 给消息设置延时时间&#xff0c;到一定时间&#xff0c;消费者才能消费的到&#xff0c;中间件内部通过每秒钟扫…

TCP的运输连接管理

TCP的运输连接管理 文章目录TCP的运输连接管理TCP报文格式简介首部各个字段的含义控制位(flags)TCP的连接建立抓包验证一些细节及解答TCP连接释放抓包验证一些细节及解答参考TCP是面向连接的协议。运输连接是用来传送TCP报文的。TCP运输连接的建立和释放时每一次面向连接的通信…

第一部分:简单句——第一章:简单句的核心——二、简单句的核心变化(主语/宾语/表语的变化)

二、简单句的核心变化 简单句的核心变化其实就是 一主一谓&#xff08;n. v.&#xff09; 表达一件事情&#xff0c;谓语动词是其中最重要的部分&#xff0c;谓语动词的变化主要有四种&#xff1a;三态加一否&#xff08;时态、语态、情态、否定&#xff09;&#xff0c;其中…

MSI_MSI-X中断之源码分析

MSI_MSI-X中断之源码分析 文章目录MSI_MSI-X中断之源码分析一、 怎么发出MSI/MSI-X中断1.1 在RK3399上体验1.1.1 安装工具1.1.2 查看设备MSI-X信息1.1.3 验证MSI-X信息二、 怎么使用MSI/MSI-X三、 MSI/MSI-X中断源码分析3.1 IRQ Domain创建流程3.1.1 GIC3.1.2 ITS3.1.3 PCI MSI…

Linux C/C++ timeout命令实现(运行具有时间限制)

Linux附带了大量命令&#xff0c;每个命令都是唯一的&#xff0c;并在特定情况下使用。Linux timeout命令的一个属性是时间限制。可以为任何命令设置时间限制。如果时间到期&#xff0c;命令将停止执行。 如何使用timeout命令 我们将解释如何使用Linux timeout命令 timeout […

七、Git远程仓库操作——团队成员内协作

1. github远程协作的两种方式 前面我写的笔记&#xff0c;都是自己一个人在玩&#xff0c;无论是本地操作还是推送到远程都是自己推送到自己的仓库。 如果是别人拥有这个仓库&#xff0c;而我想对这个仓库的内容更改后&#xff0c;然后想推送更新到这个仓库&#xff0c;我们要…

【随笔】我迟到的2022年度总结:突破零粉丝,1个月涨粉1000+,2023年目标3万+

前言 我是21年12月注册的csdn&#xff0c; 作为用户平时看看文章&#xff0c;从未参与过写文章这件事。 但这一年的时间我见证了很多新号的崛起&#xff0c;有的号我平时关注比较多&#xff0c;看着他们从零粉丝突破了三万甚至五万的粉丝量。 在csdn上遇到了我的贵人&#x…

位运算 | 1356. 根据数字二进制下 1 的数目排序

LeetCode 1356. 根据数字二进制下 1 的数目排序 给你一个整数数组 arr 。请你将数组中的元素按照其二进制表示中数字 1 的数目升序排序。如果存在多个数字二进制中 1 的数目相同&#xff0c;则必须将它们按照数值大小升序排列。 文章讲解https://www.programmercarl.com/1356.%…

【微电网】基于风光储能和需求响应的微电网日前经济调度(Python代码实现)

目录 1 概述 2 知识点及数学模型 3 算例实现 3.1算例介绍 3.2风光参与的模型求解 3.3 风光和储能参与的模型求解 3.5 风光储能和需求响应都参与模型求解 3.6 结果分析对比 4 Python代码及算例数据 1 概述 近年来&#xff0c;微电网、清洁能源等已成为全球关注的热点…

Linux Vim编辑器基础讲解

目录 vim三个模式 命令模式 输入模式&#xff08;insert 插入模式、编辑模式&#xff09; 末行模式 编辑简单文档 什么是vim Vim是文本编辑器&#xff0c;是Linux上最常用的文本编辑器 Vim可以建立、编辑、显示文件 绝大多数Linux都会携带vim或者vi vim编辑器和vi编辑器的…

windbg-应用层实时调试

调试符号windbg使用一个或多个目录来存放符号条件&#xff0c;并使用环境变量_NT_SYMBOL_PATH来指向这些环境变量的位置&#xff0c;对操作系统内部模块的符号文件&#xff0c;一般用http://msdl.microsoft.com/download/symbols配置如下&#xff1a;SRV*C:\Symbols*http://msd…

手把手教你部署ruoyi前后端分离版本

下载源码&#xff08;当前版本3.8.5&#xff09;RuoYi-Vue: &#x1f389; 基于SpringBoot&#xff0c;Spring Security&#xff0c;JWT&#xff0c;Vue & Element 的前后端分离权限管理系统&#xff0c;同时提供了 Vue3 的版本 (gitee.com)创建数据库(一定要是这三个&…

【STM32】【HAL库】遥控关灯3 遥控器

相关连接 【STM32】【HAL库】遥控关灯0 概述 【STM32】【HAL库】遥控关灯1主机 【STM32】【HAL库】遥控关灯2 分机 【STM32】【HAL库】遥控关灯3 遥控器 需求 硬件遥控器 控制一个灯的开关(2个按键),发射RF433或红外 使用纽扣电池供电 一键启动,低待机功耗 硬件设计 一键…

推荐系统开源工具RecBole学习

文章全文首发&#xff1a;码农的科研笔记&#xff08;公众号&#xff09; RecBole是由AI Box团队开发的基于Pytorch的推荐系统算法库。该框架从数据处理、模型开发和算法训练都有涉及&#xff0c;能方便进行算法构建和实验对比。 数据组织形式 RecBole约定了一个统一、易用的数…

发生异常: AttributeError ‘xxx’ object has no attribute ‘ooo’

python 发生异常: AttributeError ‘xxx’ object has no attribute ‘ooo’ 原因&#xff1a; 函数调用发生在变量定义之前 示例分析&#xff1a; 在apple.py文件中代码如下&#xff1a; class Apple():def __init__(self):self.eat()self.pricedef eat(self):print("吃…

Spring Security in Action 第十八章 手把手OAuth2应用

本专栏将从基础开始&#xff0c;循序渐进&#xff0c;以实战为线索&#xff0c;逐步深入SpringSecurity相关知识相关知识&#xff0c;打造完整的SpringSecurity学习步骤&#xff0c;提升工程化编码能力和思维能力&#xff0c;写出高质量代码。希望大家都能够从中有所收获&#…

Java:SpringMVC的使用(2)

目录第十二章 REST风格CRUD练习12.1 搭建环境12.2 实现功能思路第十三章 SpringMVC消息转换器13.1 消息转换器概述13.2 使用消息转换器处理请求报文(1) 使用RequestBody获取请求体(2) 使用HttpEntity\<T>获取请求体及请求头13.3 使用消息转换器处理响应报文(1) 使用Respo…

llvm 创建外部调用函数方法

llvm 创建外部调用函数方法 2023-02-12 15:26:19 sizaif 文章目录llvm 创建外部调用函数方法法一:声名参数类型及函数类型在llvm IR中处理并调用函数:外部函数&#xff1a;法二声明函数在llvm IR中处理并函数调用外部函数法一: 声名参数类型及函数类型 // Fun Ty static Fun…

【CS224W】(task3)NetworkX工具包实践

note 节点可以为任意可哈希的对象&#xff0c;比如字符串、图像、XML对象&#xff0c;甚至另一个Graph、自定义的节点对象。通过这种方式可以自由灵活地构建&#xff1a;图为节点、文件为节点、函数为节点&#xff0c;等灵活的图形式。暂时省略&#xff1a;【B5】计算机网络图…