腾讯:海量小文件场景下CephFS优化之路

news2024/11/25 10:37:02

Ceph开源社区 2021-02-25 17:58

摘自:https://mp.weixin.qq.com/s/rTNyzY9W3ZunroYo57tjoA

1. 背景

        随着大数据、人工智能技术的蓬勃发展,人类对于算力资源的需求也迎来大幅度的增长。在腾讯内部,星辰算力平台以降本增效为目标,整合了公司的GPU训练卡资源,为算法工程师们提供统一的底层GPU算力服务。借助于虚拟化、算力挖掘等技术,平台服务公司内各BG的AI训练场景,GPU利用率业界领先。同时,通过云原生任务化的方式,对接了内部各大业务,促进了AI技术研究效率的提升和创新研究。

        当下,由于AI训练时的高性能计算设备(如NVIDIA GPU)成本高昂,如果任务在训练过程中不能保证数据IO的速度,将会导致计算设备低载甚至空载,这无疑在时间和资源上都是一种极大的浪费。

        在星辰算力平台内部,用户的训练数据大多存放在平台提供的CephFS中,训练时将对应的CephFS目录挂载至容器内部,从而使用户在训练时能够像使用本地文件系统一样使用CephFS。但在平台运营过程中我们发现,在训练数据集文件数较多时,训练任务使用CephFS会使训练速度变得异常缓慢。基于这个普遍存在的问题,本文剖析其产生的原理,然后介绍相应的优化方案。最后,通过延伸思考来发散思维,简要介绍了不同场景下AI训练加速的技术。

2. 基本概念

2.1. CephFS IO流程

CephFS IO流程如下图所示。

图片

CephFS IO路径

        当客户端进行文件系统调用时(如openreadreaddir等),需要先从元数据服务器(Metadata ServerMDS)中获取请求文件的元数据信息,元数据信息主要包括文件的Inode号、权限、uidgid和访问更改时间等。为了加快元数据的访问效率,MDS将大部分热点元数据都缓存在自己的内存中,从而避免低效地通过访问RADOSReliable, Autonomic Distributed Object Store)层来获取元数据。客户端在从MDS中获取元数据后,通过计算的方式(CRUSH算法)得到数据在RADOS中的位置,最后与远程的存储设备进行交互。

        从这个架构来看,CephFS是一个元数据和用户数据分离的文件系统。文件的元数据和数据存储在RADOS中的不同Pool中,客户端需要先与MDS进行元数据交互,再与RADOS进行数据交互。

2.2. Ceph-FUSE

  Ceph-FUSE是CephFS客户端的一种形式,通过用户空间文件系统(Filesystem in UserspaceFUSE)的方式来实现CephFS客户端的功能。FUSE是一个面向类Unix计算机操作系统的软件接口,它使无特权的用户能够无需编辑内核代码而创建自己的文件系统。目前Linux通过内核模块对此进行支持。通过这种方式,我们可以编写用户态的应用程序,只需要实现Linux定义的一组文件系统接口,即可在用户态实现一个完整的文件系统。

当用户需要与CephFS进行交互时,客户端的整个IO流程如下:

  1. 用户程序通过syscallglibc库进行系统调用

  2. 进程陷入内核态,文件系统操作请求到达Linux虚拟文件系统(Virtual FilesystemVFS

  3. VFS根据请求类型,从Dentry CacheInode CachePage Cache中分别查找dentryinode和页缓存,若缓存命中可直接返回

  4. 若缓存不命中,则将请求转发至FUSE Driver

  5. Ceph-FUSE进程通过libfuse监听到来自于/dev/fuse的请求,与Ceph集群进行交互并返回结果。

图片

Ceph-FUSE IO路径

        当用户态程序发起FUSE请求时,Ceph-FUSE在经过处理后会将元数据信息缓存在内存中,提升后续访问的性能。同时,Linux的Dentry CacheInode CachePage Cache也会分别缓存该文件的dentryinode和页,提升热点数据的读取性能。

3. 问题

3.1. 问题源起

        星辰算力平台服务了公司内部各个BG和部门的AI算法工程师,因此平台上运行的训练任务场景也各不相同。在运营过程中我们发现,有用户反映某些任务中CephFS的读取速度较慢,使整个训练的时间拉长,其中属CV类的任务较为明显。

        平台上CV类的任务数据集,一般都是海量的图片文件。这类数据集的特点是:

  • 文件个数多,小数据集达到十万级别,大数据集达到百万、千万甚至上亿级别。

  • 单个文件占用空间不大,大多是小文件。

3.2. 理论分析

        AI训练场景与许多复杂的文件操作场景不同,其数据读写的逻辑较为简单。一般来说,用户会在每个epoch训练相同的数据,然后训练多个epoch直至模型达到收敛条件。因此,AI训练场景下,训练文件在训练过程中保持不变,且被读取的频率相对固定,同时写文件的频率较低

        针对这种特点,由于Ceph-FUSE会对访问过的元数据进行缓存,同时Linux的Dentry CacheInode CachePage Cache也会充分缓存读取过的文件元数据和文件数据。通常来说,在第二个epoch开始时,由于数据集文件在第一个epoch已被访问过,训练时的IO速度应当有非常明显的提升。然而,事与愿违,对于较多数量的文件,我们发现训练速度没有明显提升,且每个epoch的训练速度都很慢

        为了查出其中的原因,接下来我们复制一个一模一样的任务,打开Ceph-FUSE日志进行分析。

3.3. 原因排查

3.3.1. Ceph-FUSE日志分析

        在训练任务开始时,打开母机上的Ceph-FUSE日志进行查看。

疑点现象:

  1. 在第一个epoch接近末尾时,发现出现了日志trim_caps mds.x max xxx caps xxx

  2. 每次trim_caps执行,清除的dentry个数为5000。

  3. 该日志每隔5s会打印一次,往后的训练过程中会一直持续。

        注:CAPS是指capabilitiesMDSCAPS授予客户端对不同文件进行操作的许可,因此MDS需要实时维护每个客户端文件操作的CAPS。这就意味着,如果客户端持有了某个文件的CAPS并进行了缓存,MDS需要知道每个客户端缓存了哪些文件。

3.3.2. 提出猜想

根据疑点现象大概能够提出以下的猜想:

  1. 在第一个epoch结束时发生了trim_caps现象,且多次测试结果均是如此,猜测可能是缓存数量到达了某个阈值。

  2. 日志每隔5s会打印一次,可能是定时器触发了trim_caps

  3. MDS需要维护每个客户端的CAPS,当客户端读取文件数较多时,MDS的cache总会达到oversize的状态,必定会触发trim_caps

3.3.3. 代码验证

        根据上述猜想,可以在茫茫的Ceph源码中直奔主题,分别找出MDSCeph-FUSE的关键代码。

3.3.3.1. MDS端

根据现象2,在MDS中的tick函数内找到如下代码:

void MDSRankDispatcher::tick()
{
  ......
  if (is_active() || is_stopping()) {
    server->recall_client_state(nullptr, Server::RecallFlags::ENFORCE_MAX); // 选中该MDS下持有较多caps数量的客户端,执行caps回收
    mdcache->trim();
    mdcache->trim_client_leases();
    mdcache->check_memory_usage(); // 当内存使用量过大时,选中该MDS下所有客户端,执行caps回收(recall_client_state)
    mdlog->trim();
  }
  ......
}

        从中可以看出,MDS端定时对客户端的CAPS进行回收,如果回收后内存使用量仍然过高,就对所有客户端再执行一次CAPS回收。在check_memory_usage函数中会根据cache试用情况决定是否再执行recall_client_state

void MDCache::check_memory_usage()
{
  ......
  if (cache_toofull()) {
    mds->server->recall_client_state(nullptr);
  }
  ......
}

        进入关键函数recall_client_state进行查看。

/**
 * Call this when the MDCache is oversized, to send requests to the clients
 * to trim some caps, and consequently unpin some inodes in the MDCache so
 * that it can trim too.
 */
std::pair<bool, uint64_t> Server::recall_client_state(MDSGatherBuilder* gather, RecallFlags flags)
{
  ......
  const bool enforce_max = flags&RecallFlags::ENFORCE_MAX;
  const auto max_caps_per_client = g_conf->get_val<uint64_t>("mds_max_caps_per_client"); // 默认为1_M
  const auto min_caps_per_client = g_conf->get_val<uint64_t>("mds_min_caps_per_client"); // 默认为100
  const auto recall_max_caps = g_conf->get_val<uint64_t>("mds_recall_max_caps"); // 默认为5000
  ......
  /* trim caps of sessions with the most caps first */
  std::multimap<uint64_t, Session*> caps_session;
  auto f = [&caps_session, enforce_max, max_caps_per_client](Session* s) {
    auto num_caps = s->caps.size(); // 当前caps总量
    // 当flags为RecallFlags::ENFORCE_MAX时,只把caps数量超过max_caps_per_client的客户端找出来,否则找出所有客户端
    if (!enforce_max || num_caps > max_caps_per_client) {
      caps_session.emplace(std::piecewise_construct, std::forward_as_tuple(num_caps), std::forward_as_tuple(s));
    }
  };
  mds->sessionmap.get_client_sessions(std::move(f));
  ......
  for (const auto p : boost::adaptors::reverse(caps_session)) {
    ......
    // 计算每个客户端的最大caps数量
    uint64_t newlim;
    if (num_caps < recall_max_caps || (num_caps-recall_max_caps) < min_caps_per_client) {
      newlim = min_caps_per_client;
    } else {
      newlim = num_caps-recall_max_caps;
    }
    if (num_caps > newlim) {
      /* now limit the number of caps we recall at a time to prevent overloading ourselves */
      uint64_t recall = std::min<uint64_t>(recall_max_caps, num_caps-newlim); // 这里可以看出,每次最多回收mds_recall_max_caps个
      newlim = num_caps-recall;
      ......
      auto m = new MClientSession(CEPH_SESSION_RECALL_STATE); // 新建一个类型为CEPH_SESSION_RECALL_STATE的请求
      m->head.max_caps = newlim; // 设置客户端的最大caps数量
      mds->send_message_client(m, session); // 向客户端发送请求
      ......
    }
    ......
  }
  ......
}

        从上述代码基本可以确定CAPS被清除的原因,MDS每隔5s执行了一次recall_client_state。由于mds_max_caps_per_client默认被设置为1_M(也就是1048576),当训练程序读取文件个数达到1_M后该客户端就会被加入caps_session队列发起CAPS回收请求。由于recall_max_caps默认被设置为5000,所以每次CAPS回收的个数为5000

3.3.3.2. Ceph-FUSE端

首先,根据MDS端发起的类型为CEPH_SESSION_RECALL_STATE的请求,找到客户端接受请求的代码。

void Client::handle_client_session(MClientSession *m) 
{
  ......
  switch (m->get_op()) {
    ......
  case CEPH_SESSION_RECALL_STATE:
    trim_caps(session, m->get_max_caps()); // max_caps,值为上述的newlim
    break;
  ......
  }
  ......
}

        Ceph-FUSE接收到MDS的请求后,进入trim_caps函数。

void Client::trim_caps(MetaSession *s, uint64_t max)
{
  mds_rank_t mds = s->mds_num;
  size_t caps_size = s->caps.size(); // 客户端caps总量
  ......
  uint64_t trimmed = 0;
  auto p = s->caps.begin();
  std::set<Dentry *> to_trim; // 将需要执行caps回收的Dentry放入其中等待回收
  
  // 以下内容通过迭代器p将caps清理至max以下,将需要清理的Dentry放入to_trim中
  while ((caps_size - trimmed) > max && !p.end()) {
    ......
  }

  for (const auto &dn : to_trim) {
    trim_dentry(dn); // 执行Ceph-FUSE内的dentry缓存
  }
  to_trim.clear();

  caps_size = s->caps.size();
  if (caps_size > max)
    _invalidate_kernel_dcache(); // 这是关键函数,调用了Linux的remount操作来清理所有的dentries

        Ceph-FUSE接收到MDS的请求后,会将CAPS总量清理至max以下(本例中就是清理5000CAPS)。同时,将这些CAPS对应的dentry缓存全部清除,并调用操作系统命令来清除Dentry CacheInode CachePage Cache,执行命令为:

static int remount_cb(void *handle)
{
  // used for trimming kernel dcache. when remounting a file system, linux kernel
  // trims all unused dentries in the file system
  char cmd[1024];
  CephFuse::Handle *cfuse = (CephFuse::Handle *)handle;
  snprintf(cmd, sizeof(cmd), "mount -i -o remount %s", cfuse->opts.mountpoint); // 调用remount,清理文件系统的缓存
  int r = system(cmd);
  ......
}

       

3.4. 小结

至此,基本真相大白。整体流程如下图所示:

  1. 训练程序启动,开始读取文件。

  2. 在第一个epoch训练后期,Ceph-FUSE拥有的CAPS达到1_M

  3. MDS定时器触发,对持有CAPS超过1_M的客户端执行发起回收CAPS请求,回收个数为5000

  4. Ceph-FUSE接收到CEPH_SESSION_RECALL_STATE请求,从caps队列中清除5000CAPS并将这些CAPS对应的dentry从cache中清除。

  5. Ceph-FUSE调用Linux的remount命令来清除Linux文件系统的cache。

  6. MDS检查自身内存使用情况,若超过阈值则重复上述回收操作。

  7. 训练程序第二个epoch后,由于文件系统的cache被清除,导致缓存失效。

图片

CAPS回收流程

4. 解决方案

        从上述分析来看,最直观的改进方法就是将MDS端的参数mds_max_caps_per_client增大,可以使得MDS能够维护更多的CAPS。然而,这是一种治标不治本的方法。接下来提出一种Ceph-FUSE客户端缓存的方案,避免客户端CAPS清除导致训练速度变慢。

4.1. 元数据缓存方案

4.1.1. 元数据缓存

        Ceph针对的是通用场景,设计复杂的CAPS机制来保证多客户端对同一文件读写时的一致性。但在我们的场景中,读写方式却较为固定。主要表现为:

  1. 训练过程中读取的数据集在训练过程中不会发生改变,且读取频率很高。

  2. 写文件的频率较低,主要是ckptlog文件,且不会读。

        在这个特殊的场景下,可以部分牺牲一致性来获取性能上的提升。具体表现为,Ceph-FUSE侧可以将以只读方式打开的文件进行元数据缓存,减少与MDS的交互,同时在trim_caps发生时不去真正删除这部分元数据对应的缓存。核心改造如下所示:

  1. Ceph-FUSE接收到open请求时,如果以只读方式打开,则将其标记为I_CACHED状态。在该状态下的文件操作不会请求MDS获取CAPS,可以直接从本地cache中读取元数据,大大减少了与MDS的交互。

  2. 如果一个文件被只读打开后,将无法被读写打开,这是为了保证写数据的一致性。

  3. trim_caps发生时,Ceph-FUSECAPS被回收的Inode标记为I_ORPHAN状态,然后请求MDS删除这些CAPS。此时,MDS上已不存在这些Inode的缓存但是本地Ceph-FUSE并没有真正进行CAPS回收,与此同时也不去清除Linux文件系统的cache,充分保证了元数据的缓存。

图片

元数据缓存方案

        以上优化建立的前提是:只读方式打开的文件不会进行修改。在我们的AI训练场景下,训练任务完美契合了这个条件。

4.1.2. 缓存淘汰算法

        Ceph-FUSE会将元数据缓存在本地,但其缓存淘汰算法是一种带高低优先级的LRU算法。LRU算法核心思想是如果数据最近被访问过,那么将来被访问的几率也更高,但这种思想不符合AI训练的场景。在大多任务训练过程中,训练数据文件会被均匀地访问,每一个epoch中被访问过的文件反而是这个epoch中不会再被读取的文件。采用LRU算法会使缓存队列中即将被用到的文件元数据被删除,如下图所示。

图片

LRU淘汰方式

        下图模拟了LRU淘汰策略下训练数据集命中率分布曲线。

图片

LRU淘汰策略下训练数据集命中率分布

        从该图中可以看出,LRU淘汰策略下缓存队列长度越接近数据集大小,命中率提升才越明显。当队列长度只有数据集大小的一半时,命中率只有15%左右

        在AI训练的场景下,采用不替换策略(Not ReplacementNR)将是命中率最高的算法。在训练的第一个epoch时,Ceph-FUSE将元数据放到缓存中。当缓存队列已满时,Ceph-FUSE将不替换现有缓存的数据,保持缓存不变。在第二个epoch时,Ceph-FUSE从缓存队列中读取文件元数据,若未命中则请求MDS获取。

图片

NR算法

4.1.3. 优化结果

        结合两点针对Ceph-FUSE的优化改动,我们对示例任务进行了测试,得到如下的性能测试数据。

图片

训练任务测试结果

        从图中可以看出,经过优化后针对海量小文件训练场景,训练速度的提升非常明显。在第二个epoch后,元数据缓存优化版本的训练速度提升为原来的3~4倍,且训练速度较为稳定。相比于之前的版本,经过优化后的Ceph-FUSE能够充分利用Linux文件系统的cache,且避免了每个epoch与MDS之间的交互。经过优化后的版本训练速度能与本地SSD较为贴近。

4.2. 文件缓存方案

        文件缓存方案实际上是一种在元数据缓存优化的基础上,利用本地SSD对文件进行缓存的方案。针对文件数量特别多,利用Linux文件系统cache但是内存不充足的情况,该方法会有一定效果。

        训练程序在第一个epoch训练时,Ceph-FUSE在处理完read请求后将文件写入本地SSD中。为了避免海量小文件直接写入本地造成较多的lookup操作,同时也为了避免任务完成后文件缓存难以进行清理的问题,考虑将所有读取后的文件进行聚合缓存至一个本地Cache大文件中,由Ceph-FUSE来记录每个文件在本地Cache文件中的偏移。

文件缓存方案的详细步骤如下所示:

  • 文件缓存命中:

    • Metadata Cache中找出文件在本地Cache文件中的偏移。

    • 通过pread从本地SSD缓存文件中读取指定范围的字节。

  • 文件缓存不命中:

    • 按照正常流程,与Ceph集群进行交互,得到读取的字节流。

    • 写本地Cache文件,并记录该文件在其中的偏移。

    • 更新Metadata Cache,将文件元数据和偏移量加入其中。

图片

文件缓存方案

该方案虽然能够充分利用本地SSD,但也有一些缺点,具体表现为:

  1. 由于第一个epoch读取文件时,Ceph-FUSE会写本地Cache文件,可能会使得第一个epoch训练速度变慢。但当epoch数较多时这部分时间牺牲是值得的。

  2. IO路径变得更长,Ceph-FUSE需要读本地文件。

4.3. 方案对比

方案适用场景

原版

在训练过程中需要修改数据集

元数据缓存

在训练过程中不修改只读打开的文件

元数据缓存 + 文件缓存

内存紧张,无法充分使用文件系统缓存

5. 延伸方案

        上述分析和方案主要针对的是海量小文件的IO密集型计算场景,接下来发散思维,简要介绍一下多种AI加速的解决方案。

        我们将AI训练任务分为IO密集型、GPU计算密集型和CPU计算密集型三类任务。

图片

延伸思考

5.1. IO密集型任务

        IO密集型任务指的是训练瓶颈在数据IO上的任务。这类任务一般会读取较多的数据集文件,数据量较大,GPU由于数据IO的瓶颈一直处于饥饿状态,因此GPU利用率较低。总结以下几种解决方案:

一、元数据缓存

        元数据缓存方案能够将读取过的文件元数据缓存在内存中。在元数据和用户数据分离的文件系统中,高效的元数据性能对整个系统性能至关重要。在数据集只读场景下,元数据缓存可以在FUSE侧完成,也可以在用户侧完成。该方案一方面能够大大较少与元数据服务器之间的交互,缓存热点元数据,同时也能降级元数据服务器的压力。

二、文件缓存

        文件缓存方案充分利用了本地SSD进行文件缓存。在数据集只读场景下,文件缓存仍然是可以在FUSE侧完成,也可以在用户侧完成。通过缓存文件元数据并聚合小文件进行本地存储,能使训练任务的IO方式从网络IO逐渐演变为本地IO。

三、聚合数据集文件

        聚合数据集文件方案主要指的是lmdbTFRecord等技术。在这种方案下,文件数目大大减少,可以有效地缓解深度学习场景下数据存取的问题,进而提高集群资源利用率。但文件聚合存储的方式对场景有一些限制,比如:数据更新修改会相对麻烦;数据集全局shuffle比较困难,只能做部分的shuffle。

四、GPUDirect Storage

  GPUDirect Storage是NVIDIA公司在2019年推出的有关GPU显存和存储设备之间直接进行交互的技术。传统方式下磁盘中的数据需要先加载至内存中,再拷贝到GPU显存进行训练。在这项技术下,可以绕过CPU让GPU直接与存储设备进行交互,在本地或远程存储(NVMe磁盘)与GPU显存之间建立直接的数据IO路径。该方案一方面可以避免主存内数据冗余副本的产生,另一方面也缓解了CPU和内存的压力。

5.2. GPU计算密集型任务

        GPU计算密集型任务指的是训练瓶颈在GPU计算上的任务,通常需要保证数据IO和梯度同步的低延时,使得GPU时刻处于忙碌状态。简要介绍以下几种解决方案:

一、数据预取

        数据预取是最容易实现的方案。在每一个iteration计算过程中,事先对下一个或几个iteration所需的数据进行预取并预处理,保证下一个iteration开始时特征已处于就绪状态。

二、GPUDirect RDMA(Remote direct memory access)

  GPUDirect RDMAKepler GPUCUDA 5.0期间被提出,现在已得到较为广泛的支持。在多机训练过程中,这项技术能让多个GPU之间直接进行通信,同样也是避免了主存内数据冗余副本的产生,减少数据拷贝环节。配合Mellanox RDMA设备,数据可以从GPU显存经RDMA网卡发送出去,经另一台设备的RDMA网卡后传输至GPU,大大较少了IO路径。目前Horovod等分布式训练工具均以提供对GPUDirect RDMA的支持。

5.3. CPU计算密集型任务

        CPU计算密集型任务指的是训练瓶颈在CPU计算上的任务,这类任务通常的计算瓶颈在于数据的预处理。此类任务CPU处于高负载状态,但GPU利用率和磁盘IO可能并不高。有以下几种解决方案:

一、NVIDIA DALI(Data Loading Library)

  NVIDIA DALI是一个经过优化的数据加载的开源库,提供数据从磁盘加载到训练的完整pipeline。同时该库中还提供了音频处理、图像处理、视频处理等预处理方法,能够将在CPU上执行等预处理步骤放到GPU上快速执行,从而加速AI训练输入数据的预处理。

二、特征存储

        特征存储方式是一种直观有效的方案,本质是进行CPU-GPU算力分离。对于某些大规模数据集,事先利用CPU算力对原始数据进行预处理,将样本特征打包后写入云存储设备中,然后多个GPU任务均可共享这些样本特征数据。但这类方法缺点在于当特征选取发生变化时,需要重新进行预处理。

6. 总结与展望

        本文从实际训练场景出发,首先简单介绍了CephFS相关的基本概念,接着通过现象和源码分析训练过程中读取文件缓存失效的原因,然后给出了相应的解决方案。经过优化后,测试任务的训练速度能提升至原来的3~4倍。最后,通过延伸思考来发散思维,简要介绍了不同场景下AI训练加速的技术。

        未来,针对IO密集型任务,利用GPUDirect Storage和Ceph的RADOS API等技术,结合本地SSD的高速缓存,可以在用户侧探索更极致的加速方案。这种方式理论上能够拥有更快的文件读取速度,能在用户侧对文件的元数据和数据进行充分缓存,减少用户态和内核态转换,是未来可以继续研究的方向。

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

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

相关文章

数据分析 | 随机森林如何确定参数空间的搜索范围

1. 随机森林超参数 极其重要的三个超参数是必须要调整的&#xff0c;一般再加上两到三个其他超参数进行优化即可。 2. 学习曲线确定n_estimators搜索范围 首先导入必要的库&#xff0c;使用sklearn自带的房价预测数据集&#xff1a; import numpy as np import pandas as pd f…

最强自动化测试框架Playwright(18)- 执行js脚本

page.evaluate&#xff08;&#xff09; API 可以在网页上下文中运行 JavaScript 函数&#xff0c;并将结果带回 Playwright 环境。 href page.evaluate(() > document.location.href) 如果结果是 Promise 或函数是异步的&#xff0c;则计算将自动等待&#xff0c;直到解析…

虚拟现实与增强现实技术的商业应用

章节一&#xff1a;引言 随着科技的不断发展&#xff0c;虚拟现实&#xff08;Virtual Reality&#xff0c;简称VR&#xff09;与增强现实&#xff08;Augmented Reality&#xff0c;简称AR&#xff09;技术正日益成为商业领域中的重要创新力量。这两种技术为企业带来了前所未…

Android多屏幕支持-Android12

Android多屏幕支持-Android12 1、概览及相关文章2、屏幕窗口配置2.1 配置xml文件2.2 DisplayInfo#uniqueId 屏幕标识2.3 adb查看信息 3、配置文件解析3.1 xml字段读取3.2 简要时序图 4、每屏幕焦点 android12-release 1、概览及相关文章 AOSP > 文档 > 心主题 > 多屏…

206、仿真-51单片机锂电池蓄电池电压电流加按键控制开关状态Proteus仿真设计(程序+Proteus仿真+配套资料等)

毕设帮助、开题指导、技术解答(有偿)见文未 目录 一、硬件设计 二、设计功能 三、Proteus仿真图 四、程序源码 资料包括&#xff1a; 需要完整的资料可以点击下面的名片加下我&#xff0c;找我要资源压缩包的百度网盘下载地址及提取码。 方案选择 单片机的选择 方案一&a…

基于STM32CUBEMX驱动TMOS模块STHS34PF80(1)----获取ID

基于STM32CUBEMX驱动TMOS模块STHS34PF80----1.获取ID 概述样品申请视频教程所有功能接口最小系统图生成STM32CUBEMX串口配置IIC配置IO口设置串口重定向 模块地址参考demoIIC写函数IIC读函数参考程序初始化获取ID主函数 概述 STHS34PF80 是一款非冷却、工厂校准的红外运动和存在…

变压器保护高侧过流保护整定原则

电流速断保护的动作电流可按下列两个条件来选择&#xff1a; &#xff08;1&#xff09; 躲过厂用变压器负荷侧母线上短路时流过保护装置的最大短路电流。 动作电流整定为&#xff1a; IsdKkIDmax &#xff08;1-1&#xff09; 式中 Kk——可靠系数&#xff0c; 一般取1.3&…

Oracle将与Kubernetes合作推出DevOps解决方案!

导读Oracle想成为云计算领域的巨头&#xff0c;但它不是推出自己品牌的云DevOps软件&#xff0c;而是将与CoreOS在Kubernetes端展开合作。七年前&#xff0c;Oracle想要成为Linux领域的一家重量级公司。于是&#xff0c;Oracle主席拉里埃利森&#xff08;Larry Ellison&#xf…

【Python】如何判断时间序列数据是否为平稳时间序列或非平稳时间序列?

判断时间序列数据是否为平稳时间序列或非平稳时间序列&#xff0c;通常可以通过以下方法&#xff1a; &#xff08;1&#xff09;观察时间序列数据的均值和方差是否随时间变化而发生明显的改变。若均值和方差变化明显&#xff0c;则该时间序列数据可能为非平稳时间序列&#x…

章节5:Burp 扫描功能

章节5&#xff1a;Burp 扫描功能 参考资料 https://portswigger.net/burp/documentation/scanner https://portswigger.net/burp/documentation/desktop/scanning 模块总体介绍&#xff1a; https://portswigger.net/burp/vulnerability-scanner 扫描功能的使用&#xff…

深入了解 Vue 3 组件间通信机制

什么是组件&#xff1f; 在 Vue3 中&#xff0c;组件是构建应用界面的核心概念之一。组件可以看作是可复用、自包含和可组合的代码块&#xff0c;用于封装 UI 元素和相应的行为逻辑。 通俗来说就是&#xff0c;组件&#xff08;Component&#xff09;是一种对数据和方法的简单…

置信域策略优化Trust Region Policy Optimization (TRPO)

1. 置信域方法(Trust Region Methods) [1]将置信域方法用到强化学习中&#xff0c;并取到了非常好的结果. 1.1 优化问题 1.2 置信域 1.3 置信域方法的过程 References [1] Schulman J, Levine S, Abbeel P, et al. Trust region policy optimization[C]//International conf…

旋转图像(旋转矩阵)

原题链接 旋转图像备战技术面试&#xff1f;力扣提供海量技术面试资源&#xff0c;帮助你高效提升编程技能&#xff0c;轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/rotate-image/ 算法分析 若矩阵的行列数为N&#xff0c;设i表示行索引&#xff0c;i属…

如何定位线上CPU飙高的问题

1.问题情景 我们的接口卡死&#xff0c;CPU飙高到打不开的网页 2.问题定位 2.1 top指令 通过top命令找到CPU耗用最厉害的那个进程的PID 直接输入top Linux下的100%代表一个核心&#xff0c;如果是八核&#xff0c;最高可以到800%&#xff0c;这样才算满 然后通过PID找到CP…

设计模式再探——策略模式

目录 一、背景介绍二、思路&方案三、过程1.策略模式简介2.策略模式的类图3.策略模式代码4.策略模式还可以优化的地方5.策略模式的例子改造(配置文件反射) 四、总结五、升华 一、背景介绍 最近在做产品的过程中&#xff0c;对于主题讨论回复内容&#xff0c;按照追评次数排…

Java学习手册——第一篇Java简介

今后Java学习手册就来给大家梳理JavaSE的基础知识啦&#xff0c; 除了这个专栏我们还有其他专栏&#xff1a;前端、安全、后端等。 希望大家可以在这里一起讨论学习哟~ Java学习手册——第一篇Java简介 1. Java基础知识2. Java能干嘛3. Java基础环境搭建 1. Java基础知识 出生…

BANI时代下的项目管理:如何迎接挑战与机遇

BANI时代的介绍与特点 BANI时代的起源 BANI这个概念首次被提出是为了描述和理解我们所处的复杂、多变、不确定的时代。与VUCA&#xff08;Volatile, Uncertain, Complex, Ambiguous&#xff09;相比&#xff0c;BANI更加准确地捕捉了我们面临的现实挑战。VUCA重点关注外部环境…

【基础操作】Linux打开terminal,Anaconda默认进入的虚拟环境(python版本)设置(自行指定)

为了免除每次打开terminal都要输入 conda activate … 的麻烦&#xff0c;可以这么设置。 1. 打开terminal&#xff0c;然后输入命令 vim ~/.bashrc2. 然后在文件末尾添加 conda activate your_envs # your_envs是你的虚拟环境名称3. 保存退出&#xff0c;重新打开就成功啦…

第八课 双重所有格和不定代词

系列文章目录 文章目录 系列文章目录前言一、of s 的所有格1、of 有生命的名词 ’s2、of 名词性物主代词3、小结 二、反身代词1、作宾语和介词宾语2、作表语3、作固定惯语 三、相互代词四、指示代词 Such 和 Same 的用法 前言 一、of s 的所有格 1、of 有生命的名词 ’s 2、…

大数据课程I3——Kafka的消息流与索引机制

文章作者邮箱:yugongshiye@sina.cn 地址:广东惠州 ▲ 本章节目的 ⚪ 掌握Kafka的消息流处理; ⚪ 掌握Kafka的索引机制; ⚪ 掌握Kafka的消息系统语义; 一、Kafka消息流处理 1. Producer 写入消息 流程说明: 1. producer 要向Kafka生产消息,需要先通过…