以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。

并行收集:
指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。

并发收集:
指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上

吞吐量:
即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%

串行Serial / Serial Old 收集器

特点:单线程、简单高效(与其他收集器的单线程相比),采用
复制算法
。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)参数:-XX:+UseSerialGC -XX:+UseSerialOldGC

安全点:
让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态

Serial Old
是Serial收集器的老年代版本:采用
标记整理
算法

特点:

  • 单线程收集器


    • 收集效率高,不会产生对象引用变更

    • STW时间长

  • 使用场景:适合内存小几十兆以内,比较适合简单的服务或者单CPU服务,避免了线程交互的开销。

  • 优点:小堆内存且单核CPU执行效率高。

  • 缺点:堆内存大,多核CPU不适合,回收时长非常长。

ParNew 收集器

年轻代:-XX:+UserParNewGC 老年代搭配 CMS

ParNew收集器其实就是Serial收集器的多线程版本

  • 特点:
    多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。和Serial收集器一样存在Stop The World问题

CMS 收集器

老年代:-XX:+UserConcMarkSweepGC年轻代搭配ParNew

Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器

  • 特点:
    基于
    标记清除算法
    实现。并发收集、低停顿,但是会产生内存碎片

运行过程分分为下列4步:

  1. 初始标记:
    标记GCRoots直接关联的对象以及年轻代指向老年代的对象,会发生Stop the word。但是这个阶段的速度很快,因为没有向下追溯,即只标记一层。

例如:Math math = new Math();此时new Math()即为math的直接引用对象,再往下为间接引用不做记录,例如构造方法中引用了其他成员变量

  1. 并发标记:
    接着从gc roots的直接引用对象开始遍历整条引用链并进行标记,此过程耗时较长,但无需停顿用户线程,可与垃圾收集线程一起并发运行。由于用户线程继续运行,因此可能会导致已经标记过的对象状态发生改变,这个阶段采用
    三色标记算法
    , 在对象头(Mark World)标识了一个颜色属性,不同的颜色代表不同阶段,扫描过程中给与对象一个颜色,记录扫描位置,防止cpu时间片切换不需要重新扫描。
  2. 重新标记:
    为了
    修正并发标记
    期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题,这里会慢一些
  3. 并发清理:
    标记结束之后开启用户线程,同时垃圾收集线程也开始对未标记的区域进行清除,此阶段若有新增对象则会被标记为黑色,不做任何处理

在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。

  • 应用场景:
    适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务

  • 优点:


    • 并发收集;

    • STW时间相对短,低停顿;

  • 缺点:


    • 吞吐量低: 低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
    • 内存碎片问题:CMS本质上是实现了标记清除算法的收集器,这意味着会产生内存碎片,当碎片化非常严重的时候,这时候有大对象进入无法分配内存时会触发FullGC,特殊场景下会使用Serial收集器,导致停顿不可控。
    • 无法处理浮动垃圾,需要预留空间,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS,会导致停顿时间更长

三色标记算法

三色标记

  • 黑色:代表了自己已经被扫描完毕,并且自己的引用对象也已经确定完毕。

  • 灰色:代表自己已经被扫描完毕了, 但是自己的引用还没标记完。

  • 白色:则代表还没有被扫描过。标记过程结束后,所有未被标记的对象都是不可达的,可以被回收。

三色标记算法的
问题场景
:当业务线程做了对象引用变更,会发生B对象不会被扫描,当成垃圾回收。

public class Demo3 {
 
    public static void main(String[] args) {
        R r = new R();
        r.a = new A();
        B b = new B();
        // GCroot遍历R, R为黑色, R下面的a引用链还未扫完置灰灰色,R.b无引用, 切换时间分片
        r.a.b = b;
        // 业务线程发生了引用改变, 原本r.a.b的引用置为null
        r.a.b = null;
        // GC线程回来继续上次扫描,发现r.a.b无引用,则认为b对象无任何引用清除
        r.b = b;
        // GC 回收了b, 业务线程无法使用b
    }
}
 
class R {
    A a;
    B b;
}
 
class A {
    B b;
}
 
class B {
}

当GC线程标记A时,CPU时间片切换,业务线程进行了对象引用改变,这时候时间片回到了GC线程,继续扫描对象A, 发现A没有任何引用,则会将A赋值黑色扫描完毕,这样B则不会被扫描,会标记B是垃圾, 在清理阶段将B回收掉,错误的回收正常的对象,发生业务异常。

CMS基于这种错误标记的解决方案是采取
写屏障 + 增量更新Incremental Update
, 在业务线程发生对象变化时,重新将R标识为灰色,重新扫描一遍,Incremental Update 在特殊场景下还是会产生漏标。即当黑色对象被新增一个白色对象的引用的时候,记录下发生引用变更的黑色对象,并将它重新改变为灰色对象,重新标记。

public class Demo3 {
 
    public static void main(String[] args) {
        // Incremental Update还会产生的问题
        R r = new R();
        A a = new A();
        A b = new A();
        r.a1 = a;
        // GC线程切换, r扫完a1, 但是没有扫完a2, 还是灰色
        r.a2 = b;
        // 业务线程发生引用切换, r置灰灰色(本身灰色)
        r.a1 = b;
        // GC线程继续扫完a2, R为黑色, b对象又漏了~
    }
}
 
class R {
    A a1;
    A a2;
}
 
class A {
}

当GC 1线程正在标记O, 已经标记完O的属性 O.1, 准备标记O.2时,业务线程把属性O,1 = B,这时候将O对象再次标记成灰色, GC 1线程切回,将O.2线程标记完成,这时候认为O已经全部标记完成,O标记为黑色, B对象产生了漏标, CMS针对Incremental Update产生的问题,只能在remark阶段,暂停所有线程,将这些发生过引用改变过的,重新扫描一遍。

吞吐量优先Parallel

  • 多线程

  • 堆内存较大,多核CPU

  • 单位时间内,STW(stop the world,停掉其他所有工作线程)时间最短

  • JDK1.8默认使用的垃圾回收器

Parallel Scavenge 收集器

新生代收集器,基于
复制算法
实现的收集器。特点是吞吐量优先,故也称为吞吐量优先收集器,能够并行收集的多线程收集器,允许多个垃圾回收线程同时运行,降低垃圾收集时间,提高吞吐量。 Parallel Scavenge 收集器关注点是吞吐量,高效率的利用 CPU 资源。 CMS 垃圾收集器关注点更多的是用户线程的停顿时间。

Parallel Scavenge 收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的

-XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的-XX:GCTimeRatio 参数。

  • -XX:MaxGCPauseMillis 参数的值是一个大于0的毫秒数,收集器将尽量保证内存回收花费的时间不超过用户设定值。

  • -XX:GCTimeRatio 参数的值大于0小于100,即垃圾收集时间占总时间的比率,相当于吞吐量的倒数。

该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:
GC自适应调节策略
(与ParNew收集器最重要的一个区别)

GC自适应调节策略:
Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。

  • 使用场景:适用于内存在几个G之间,适用于后台计算服务或者不需要太多交互的服务,保证吞吐量的服务。

  • 优点:可控吞吐量、保证吞吐量,并行收集。

  • 缺点:回收期间STW,随着堆内存增大,回收暂停时间增大。

Parallel Old 收集器

是Parallel Scavenge收集器的老年代版本

特点:多线程,采用
标记整理算法
(老年代没有幸存区)

  • 响应时间优先

  • 多线程

  • 堆内存较大,多核CPU

  • 尽可能让单次STW时间变短(尽量不影响其他线程运行)

面试题专栏

Java面试题专栏
已上线,欢迎访问。

  • 如果你不知道简历怎么写,简历项目不知道怎么包装;
  • 如果简历中有些内容你不知道该不该写上去;
  • 如果有些综合性问题你不知道怎么答;

那么可以私信我,我会尽我所能帮助你。

how-to-use-gpu

本文主要分享在不同环境,例如裸机、Docker 和 Kubernetes 等环境中如何使用 GPU。

跳转阅读原文:
GPU 环境搭建指南:如何在裸机、Docker、K8s 等环境中使用 GPU

1. 概述

仅以比较常见的 NVIDIA GPU 举例,系统为 Linux,对于其他厂家的 GPU 设备理论上流程都是一样的。


省流:

  • 对于裸机环境,只需要安装对应的 GPU Driver 以及 CUDA Toolkit 。

  • 对应 Docker 环境,需要额外安装 nvidia-container-toolkit 并配置 docker 使用 nvidia runtime。

  • 对应 k8s 环境,需要额外安装对应的 device-plugin 使得 kubelet 能够感知到节点上的 GPU 设备,以便 k8s 能够进行 GPU 管理。

注:一般在 k8s 中使用都会直接使用 gpu-operator 方式进行安装,本文主要为了搞清各个组件的作用,因此进行手动安装。

ps;下一篇分享下如何使用 gpu-operator 快速完成安装

2. 裸机环境

裸机中要使用上 GPU 需要安装以下组件:

  • GPU Driver
  • CUDA Toolkit

二者的关系如 NVIDIA 官网上的这个图所示:

components-of-cuda

GPU Driver 包括了 GPU 驱动和 CUDA 驱动,CUDA Toolkit 则包含了 CUDA Runtime。

GPU 作为一个 PCIE 设备,只要安装好之后,在系统中就可以通过 lspci 命令查看到,先确认机器上是否有 GPU:

root@test:~# lspci|grep NVIDIA
3b:00.0 3D controller: NVIDIA Corporation TU104GL [Tesla T4] (rev a1)
86:00.0 3D controller: NVIDIA Corporation TU104GL [Tesla T4] (rev a1)

可以看到,该设备有两张 Tesla T4 GPU。

安装驱动

首先到
NVIDIA 驱动下载
下载对应的显卡驱动:

search-gpu-driver

最终下载得到的是一个
.run
文件,例如
NVIDIA-Linux-x86_64-550.54.14.run

然后直接 sh 方式运行该文件即可

sh NVIDIA-Linux-x86_64-550.54.14.run

接下来会进入图形化界面,一路选择 yes / ok 就好

运行以下命令检查是否安装成功

nvidia-smi

如果出现显卡信息则是安装成功,就像这样:

root@test:~ nvidia-smi
Wed Jul 10 05:41:52 2024
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 535.161.08             Driver Version: 535.161.08   CUDA Version: 12.2     |
|-----------------------------------------+----------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |         Memory-Usage | GPU-Util  Compute M. |
|                                         |                      |               MIG M. |
|=========================================+======================+======================|
|   0  Tesla T4                       On  | 00000000:3B:00.0 Off |                    0 |
| N/A   51C    P0              29W /  70W |  12233MiB / 15360MiB |      0%      Default |
|                                         |                      |                  N/A |
+-----------------------------------------+----------------------+----------------------+
|   1  Tesla T4                       On  | 00000000:86:00.0 Off |                    0 |
| N/A   49C    P0              30W /  70W |   6017MiB / 15360MiB |      0%      Default |
|                                         |                      |                  N/A |
+-----------------------------------------+----------------------+----------------------+

+---------------------------------------------------------------------------------------+
| Processes:                                                                            |
|  GPU   GI   CI        PID   Type   Process name                            GPU Memory |
|        ID   ID                                                             Usage      |
|=======================================================================================|
|   
+---------------------------------------------------------------------------------------+

至此,我们就安装好 GPU 驱动了,系统也能正常识别到 GPU。

这里显示的 CUDA 版本表示当前驱动最大支持的 CUDA 版本。

安装 CUDA Toolkit

对于深度学习程序,一般都要依赖
CUDA
环境,因此需要在机器上安装
CUDA Toolkit

也是到
NVIDIA CUDA Toolkit 下载
下载对应的安装包,选择操作系统和安装方式即可

download-cuda-toolkit

和安装驱动类似,也是一个 .run 文件

# 下载安装文件
wget https://developer.download.nvidia.com/compute/cuda/12.2.0/local_installers/cuda_12.2.0_535.54.03_linux.run

# 开始安装
sudo sh cuda_12.2.0_535.54.03_linux.run

注意:之前安装过驱动了,这里就不再安装驱动,仅安装 CUDA Toolkit 相关组件

安装完成后输出如下:

root@iZbp15lv2der847tlwkkd3Z:~# sudo sh cuda_12.2.0_535.54.03_linux.run
===========
= Summary =
===========

Driver:   Installed
Toolkit:  Installed in /usr/local/cuda-12.2/

Please make sure that
 -   PATH includes /usr/local/cuda-12.2/bin
 -   LD_LIBRARY_PATH includes /usr/local/cuda-12.2/lib64, or, add /usr/local/cuda-12.2/lib64 to /etc/ld.so.conf and run ldconfig as root

To uninstall the CUDA Toolkit, run cuda-uninstaller in /usr/local/cuda-12.2/bin
To uninstall the NVIDIA Driver, run nvidia-uninstall
Logfile is /var/log/cuda-installer.log

根据提示配置下 PATH

# 添加 CUDA 12.2 到 PATH
export PATH=/usr/local/cuda-12.2/bin:$PATH

# 添加 CUDA 12.2 的 lib64 到 LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/usr/local/cuda-12.2/lib64:$LD_LIBRARY_PATH

执行以下命令查看版本,确认安装成功

root@iZbp15lv2der847tlwkkd3Z:~# nvcc -V
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2023 NVIDIA Corporation
Built on Tue_Jun_13_19:16:58_PDT_2023
Cuda compilation tools, release 12.2, V12.2.91
Build cuda_12.2.r12.2/compiler.32965470_0

测试

我们使用一个简单的 Pytorch 程序来检测 GPU 和 CUDA 是否正常。

整个调用链大概是这样的:

cuda-call-flow

使用下面代码来测试能够正常使用,
check_cuda_pytorch.py
内容如下:

import torch

def check_cuda_with_pytorch():
    """检查 PyTorch CUDA 环境是否正常工作"""
    try:
        print("检查 PyTorch CUDA 环境:")
        if torch.cuda.is_available():
            print(f"CUDA 设备可用,当前 CUDA 版本是: {torch.version.cuda}")
            print(f"PyTorch 版本是: {torch.__version__}")
            print(f"检测到 {torch.cuda.device_count()} 个 CUDA 设备。")
            for i in range(torch.cuda.device_count()):
                print(f"设备 {i}: {torch.cuda.get_device_name(i)}")
                print(f"设备 {i} 的显存总量: {torch.cuda.get_device_properties(i).total_memory / (1024 ** 3):.2f} GB")
                print(f"设备 {i} 的显存当前使用量: {torch.cuda.memory_allocated(i) / (1024 ** 3):.2f} GB")
                print(f"设备 {i} 的显存最大使用量: {torch.cuda.memory_reserved(i) / (1024 ** 3):.2f} GB")
        else:
            print("CUDA 设备不可用。")
    except Exception as e:
        print(f"检查 PyTorch CUDA 环境时出现错误: {e}")

if __name__ == "__main__":
    check_cuda_with_pytorch()

先安装下 torch

pip install torch

运行一下

python3 check_cuda_pytorch.py

正常输出应该是这样的:

检查 PyTorch CUDA 环境:
CUDA 设备可用,当前 CUDA 版本是: 12.1
PyTorch 版本是: 2.3.0+cu121
检测到 1 个 CUDA 设备。
设备 0: Tesla T4
设备 0 的显存总量: 14.75 GB
设备 0 的显存当前使用量: 0.00 GB
设备 0 的显存最大使用量: 0.00 GB

3. Docker 环境

上一步中我们已经在裸机上安装了 GPU Driver,CUDA Toolkit 等工具,实现了在宿主机上使用 GPU。

现在希望在 Docker 容器中使用 GPU,需要怎么处理呢?

为了让 Docker 容器中也能使用 GPU,大致步骤如下:

  • 1)安装
    nvidia-container-toolkit
    组件
  • 2)
    docker
    配置使用
    nvidia-runtime
  • 3)启动容器时增加
    --gpu
    参数

安装 nvidia-container-toolkit

NVIDIA Container Toolkit 的主要作用是将 NVIDIA
GPU
设备挂载到容器中。

兼容生态系统中的任意容器运行时,docker、containerd、cri-o 等。

NVIDIA 官方安装文档:
nvidia-container-toolkit-install-guide

对于 Ubuntu 系统,安装命令如下:

# 1. Configure the production repository
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
  && curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
    sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
    sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

# Optionally, configure the repository to use experimental packages 
sed -i -e '/experimental/ s/^#//g' /etc/apt/sources.list.d/nvidia-container-toolkit.list

# 2. Update the packages list from the repository
sudo apt-get update

# 3. Install the NVIDIA Container Toolkit packages
sudo apt-get install -y nvidia-container-toolkit

配置使用该 runtime

支持 Docker, Containerd, CRI-O, Podman 等 CRI。

具体见官方文档
container-toolkit#install-guide

这里以 Docker 为例进行配置:

旧版本需要手动在
/etc/docker/daemon.json
中增加配置,指定使用 nvidia 的 runtime。

    "runtimes": {
        "nvidia": {
            "args": [],
            "path": "nvidia-container-runtime"
        }
    }

新版 toolkit 带了一个
nvidia-ctk
工具,执行以下命令即可一键配置:

sudo nvidia-ctk runtime configure --runtime=docker

然后重启 Docker 即可

sudo systemctl restart docker

测试

安装
nvidia-container-toolkit
后,整个调用链如下:

nv-container-runtime-call-flow

调用链从 containerd --> runC 变成 containerd --> nvidia-container-runtime --> runC 。

然后 nvidia-container-runtime 在中间拦截了容器 spec,就可以把 gpu 相关配置添加进去,再传给 runC 的 spec 里面就包含 gpu 信息了。

Docker 环境中的 CUDA 调用大概是这样的:

cuda-call-in-container.png

从图中可以看到,CUDA Toolkit 跑到容器里了,因此宿主机上不需要再安装 CUDA Toolkit。

使用一个带 CUDA Toolkit 的镜像即可。

最后我们启动一个 Docker 容器进行测试,其中命令中增加
--gpu
参数来指定要分配给容器的 GPU。

--gpu
参数可选值:

  • --gpus all
    :表示将所有 GPU 都分配给该容器
  • --gpus "device=<id>[,<id>...]"
    :对于多 GPU 场景,可以通过 id 指定分配给容器的 GPU,例如 --gpu "device=0" 表示只分配 0 号 GPU 给该容器
    • GPU 编号则是通过
      nvidia-smi
      命令进行查看

这里我们直接使用一个带 cuda 的镜像来测试,启动该容器并执行
nvidia-smi
命令

docker run --rm --gpus all  nvidia/cuda:12.0.1-runtime-ubuntu22.04 nvidia-smi

正常情况下应该是可以打印出容器中的 GPU 信息的。

4. k8s 环境

更进一步,在 k8s 环境中使用 GPU,则需要在集群中部署以下组件:

  • gpu-device-plugin
    用于管理 GPU,device-plugin 以 DaemonSet 方式运行到集群各个节点,以感知节点上的 GPU 设备,从而让 k8s 能够对节点上的 GPU 设备进行管理。
  • gpu-exporter
    :用于监控 GPU

各组件关系如下图所示:

k8s-gpu-manual-instll-vs-gpu-operator

  • 左图为手动安装的场景,只需要在集群中安装 device-plugin 和 监控即可使用。

  • 右图为使用 gpu-operotar 安装场景,本篇暂时忽略

大致工作流程如下:

  • 每个节点的 kubelet 组件维护该节点的 GPU 设备状态(哪些已用,哪些未用)并定时报告给调度器,调度器知道每一个节点有多少张 GPU 卡可用。
  • 调度器为 pod 选择节点时,从符合条件的节点中选择一个节点。
  • 当 pod 调度到节点上后,kubelet 组件为 pod 分配 GPU 设备 ID,并将这些 ID 作为参数传递给 NVIDIA Device Plugin
  • NVIDIA Device Plugin 将分配给该 pod 的容器的 GPU 设备 ID 写入到容器的环境变量 NVIDIA_VISIBLE_DEVICES中,然后将信息返回给 kubelet。
  • kubelet 启动容器。
  • NVIDIA Container Toolkit 检测容器的 spec 中存在环境变量 NVIDIA_VISIBLE_DEVICES,然后根据环境变量的值将 GPU 设备挂载到容器中。

在 Docker 环境我们在启动容器时通过
--gpu
参数手动指定分配给容器的 GPU,k8s 环境则由 device-plugin 自行管理。

安装 device-plugin

device-plugin 一般由对应的 GPU 厂家提供,比如 NVIDIA 的
k8s-device-plugin

安装其实很简单,将对应的 yaml apply 到集群即可。

kubectl create -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.15.0/deployments/static/nvidia-device-plugin.yml

就像这样

root@test:~# kgo get po -l app=nvidia-device-plugin-daemonset
NAME                                   READY   STATUS    RESTARTS   AGE
nvidia-device-plugin-daemonset-7nkjw   1/1     Running   0          10m

device-plugin 启动之后,会感知节点上的 GPU 设备并上报给 kubelet,最终由 kubelet 提交到 kube-apiserver。

因此我们可以在 Node 可分配资源中看到 GPU,就像这样:

root@test:~# k describe node test|grep Capacity -A7
Capacity:
  cpu:                48
  ephemeral-storage:  460364840Ki
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             98260824Ki
  nvidia.com/gpu:     2
  pods:               110

可以看到,除了常见的 cpu、memory 之外,还有
nvidia.com/gpu
, 这个就是 GPU 资源,数量为 2 说明我们有两张 GPU。

安装 GPU 监控

除此之外,如果你需要监控集群 GPU 资源使用情况,你可能还需要安装
DCCM exporter
结合 Prometheus 输出 GPU 资源监控信息。

helm repo add gpu-helm-charts \
  https://nvidia.github.io/dcgm-exporter/helm-charts
  
 helm repo update
 
 
 helm install \
    --generate-name \
    gpu-helm-charts/dcgm-exporter

查看 metrics

curl -sL http://127.0.0.1:8080/metrics
# HELP DCGM_FI_DEV_SM_CLOCK SM clock frequency (in MHz).# TYPE DCGM_FI_DEV_SM_CLOCK gauge# HELP DCGM_FI_DEV_MEM_CLOCK Memory clock frequency (in MHz).# TYPE DCGM_FI_DEV_MEM_CLOCK gauge# HELP DCGM_FI_DEV_MEMORY_TEMP Memory temperature (in C).# TYPE DCGM_FI_DEV_MEMORY_TEMP gauge
...
DCGM_FI_DEV_SM_CLOCK{gpu="0", UUID="GPU-604ac76c-d9cf-fef3-62e9-d92044ab6e52",container="",namespace="",pod=""} 139
DCGM_FI_DEV_MEM_CLOCK{gpu="0", UUID="GPU-604ac76c-d9cf-fef3-62e9-d92044ab6e52",container="",namespace="",pod=""} 405
DCGM_FI_DEV_MEMORY_TEMP{gpu="0", UUID="GPU-604ac76c-d9cf-fef3-62e9-d92044ab6e52",container="",namespace="",pod=""} 9223372036854775794
...

测试

在 k8s 创建 Pod 要使用 GPU 资源很简单,和 cpu、memory 等常规资源一样,在 resource 中 申请即可。

比如,下面这个 yaml 里面我们就通过 resource.limits 申请了该 Pod 要使用 1 个 GPU。

apiVersion: v1
kind: Pod
metadata:
  name: gpu-pod
spec:
  restartPolicy: Never
  containers:
    - name: cuda-container
      image: nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda10.2
      resources:
        limits:
          nvidia.com/gpu: 1 # requesting 1 GPU

这样 kueb-scheduler 在调度该 Pod 时就会考虑到这个情况,将其调度到有 GPU 资源的节点。

启动后,查看日志,正常应该会打印 测试通过的信息。

kubectl logs gpu-pod
[Vector addition of 50000 elements]
Copy input data from the host memory to the CUDA device
CUDA kernel launch with 196 blocks of 256 threads
Copy output data from the CUDA device to the host memory
Test PASSED
Done

至此,在 k8s 环境中也可以使用 GPU 了。


【Kubernetes 系列】
持续更新中,搜索公众号【
探索云原生
】订阅,阅读更多文章。


5. 小结

本文主要分享了在裸机、Docker 环境、k8s 环境中如何使用 GPU。

  • 对于裸机环境,只需要安装对应的 GPU Driver 即可。

  • 对应 Docker 环境,需要额外安装
    nvidia-container-toolkit
    并配置 docker 使用 nvidia runtime。

  • 对应 k8s 环境,需要额外安装对应的
    device-plugin
    使得 kubelet 能够感知到节点上的 GPU 设备,以便 k8s 能够进行 GPU 管理。

现在一般都是在 k8s 环境中使用,为了简化安装步骤, NVIDIA 也提供了
gpu-operator
来简化安装部署,后续分享一下如何使用
gpu-operator
来快速安装。

来源:晓飞的算法工程笔记 公众号,转载请注明出处

论文: Anytime Continual Learning for Open Vocabulary Classification

创新点


  • 在线训练时,每个批次由新训练样本和类别平衡的存储样本组成。
  • 在线学习每个标签的准确性,以有效对原始模型和调整后模型的预测进行加权。
  • 损失修改以支持“以上皆非”(不在预设标签内)的预测,这也使开放词汇训练更加稳定。
  • 中间层特征压缩,减少训练样本的存储并提高速度,同时对准确性的影响不大。

内容概述


论文提出了针对开放词汇图像分类的任意持续学习(
AnytimeCL
)方法,旨在突破批量训练和严格模型的限制,要求系统能够在任何时间预测任何一组标签,并在任何时间接收到一个或多个训练样本时高效地更新和改进。

AnytimeCL
基于一种动态加权机制,结合了部分微调的模型的预测与原始的模型的预测。当有新训练样本时,用存储的样本填充一个类别平衡的批次更新微调模型最后的
Transformer
块,然后更新对给定标签的调优和原始模型准确度的估计,最后根据它们对每个标签的预期准确度对调优模型和原始模型的预测进行加权。

此外,论文还提出了一种基于注意力加权的主成分分析(
PCA
)的训练特征压缩方法,这减少了存储和计算的需求,对模型准确度几乎没有影响。

AnytimeCL


论文旨在通过将微调模型与原始模型相结合来增强开放词汇图像分类器以学习目标任务。调优后的模型使用与原始模型相同的编码器,但包含一个可训练的解码器。

对于一幅图像
\(x\)
,调优模型和原始模型都生成所有候选标签的概率,分别表示为
\(P_t(y|x)\)

\(P_o(y|x)\)
,最终概率通过在线类别加权(
OCW
)进行加权:

\[\begin{equation}
\label{eq:our_weighting}
P(y|x) = \alpha_o(y) P_t(y|x) + \alpha_t(y) P_o(y|x),
\end{equation}
\]

在训练过程中,新样本被编码为中间特征(图像块的特征向量加上一个
CLS
标记),可以选择进行压缩并存储,以便在未来重复使用。

模型

  • 原始模型

原始模型是公开可用的
CLIP ViT
模型,该模型基于图像嵌入
\(e_{x}\)

CLS
标记)与文本嵌入
\(e_{y}\)
的点积,为图像
\(x\)
生成给定一组候选文本标签
\(\mathcal{Y}\)
的标签
\(y\)
的概率:

\[\begin{equation}
\label{eq:class_wise_probability}
P_o(y|x) = \frac{\exp(100 \cdot \cos(e_{x}, e_{y}))}{\sum_{y_k\in\mathcal{Y}} \exp(100 \cdot \cos(e_{x}, e_{y_k}))}.
\end{equation}
\]

  • 调优模型

调优模型仅调优最后的图像
Transformer
块,同时保持标签嵌入固定。这有助于特征与文本模态保持相关,并减少对接收标签的过拟合。

给定一个新样本,构造一个包含该样本的批次以及经过类平衡采样的存储训练样本。此外,使用一种正则化损失来帮助提高性能。如果真实标签不在候选标签中,那么每个候选标签都应该预测一个较低的分数。通过在候选集中添加一个“其他”选项来实现这一点,但由于“其他”没有具体的表现,仅用一个可学习的偏差项来对其建模。因此,训练调优模型的综合损失为:

\[\begin{equation}
\label{eq:final_loss}
\mathcal{L}(x, y, \mathcal{Y}) =\mathcal{L}_{\text{ce}}(x,y,\mathcal{Y} \cup \text{other}) + \beta \mathcal{L}_{\text{ce}}(x,\text{other},(\mathcal{Y} \cup \text{other}) \setminus y),
\end{equation}
\]

在线类别加权(
OCW

在更新之前使用每个训练样本,根据调优和原始预测来更新对其标签正确性的可能性估计,从而对给定标签正确的模型分配更高的权重。应用指数滑动平均(
EMA
)更新方法在线估计它们,符合随时持续学习的目标。假设
EMA
衰减设置为
\(\eta\)
(默认为
\(0.99\)
),当前步骤调优模型的估计准确性为:

\[\begin{equation}
c_t(y) = \eta \hat{c}_t(y) + (1 - \eta) \mathbb{1}[y_t(x)=y].
\end{equation}
\]

这里,
\(\hat{c}_t(y)\)
是前一步骤中标签
\(y\)
的估计准确性;
\(y_t(x)\)
表示调优模型对
\(x\)
的预测标签。由于指数滑动平均依赖于过去的值,将
\(c_t(y)\)
计算为前
\(\lfloor \frac{1}{1-\eta} \rfloor\)
个样本的平均准确性。
\(c_o(y)\)
也是以相同的方式更新的。

在获得
\(c_t(y)\)

\(c_o(y)\)
之后,两个模型的权重为:

\[\begin{equation}
\label{eq:final_alpha}
\alpha_t(y)= \frac{c_t(y)}{c_t(y) + c_o(y) + \epsilon}, \qquad \alpha_o(y)= 1 - \alpha_t(y).
\end{equation}
\]

这里,
\(\epsilon\)
是一个非常小的数(
1e-8
),用于防止除以零。对于调优模型未见过的标签,设置
\(\alpha_t(y)=0\)
,因此
\(\alpha_o(y)=1\)

存储的高效性与隐私性

模型的调优需要存储每个图像或者存储输入到调优部分的特征(或标记)。存储图像存在缺乏隐私和在空间和计算上低效的缺点,因为在训练中需要重新编码。存储特征可以缓解其中一些问题,但仍然使用大量内存或存储空间。

训练良好的网络学习到的数据高效表示往往难以压缩,如果尝试使用在某个数据集上训练的
VQ-VAE

PCA
(主成分分析)来压缩特征向量,将无法在不大幅损失训练性能的情况下实现任何有意义的压缩。然而,每幅图像中的特征包含许多冗余。因此,计算每幅图像中特征的
PCA
向量,并将这些向量与每个特征向量的系数一起存储。

此外,并非所有标记在预测中都是同等重要的。因此,可以训练一个逐图像的注意力加权
PCA
,通过每个标记与
CLS
标记之间的注意力加权。最后,可以通过存储每个向量及其系数的最小/最大浮点值,并将它们量化为
8
位或
16
位无符号整数来进一步压缩。通过以这种方式仅存储五个
PCA
向量及其系数,可以将
50

768
维标记(
\(7\times 7\)
patch 标记 +
CLS
标记)的存储从
153K
字节减少到
5K
字节,同时预测准确度的差异不到
1%

主要实验




如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

相关:

对于特定的游戏问题使用启发式算法可以取得比AI算法更好的表现

Using A.I. to DOMINATE NERDS in TETRIS

Machine Learning: AI Learns To Play Tetris with Convolutional Neural Network

AI learns to play Tetris using Machine Learning and Convolutional Neural Network

A.I. Learns to Play Tetris

I Created An A.I. to DESTROY Tetris


启发式算法 2048

人类: left, right, down,rotate,left,right,rotate

机器:rotate,right,right,right,all down rotate, left,left,all down

image-20241106124612748

image-20241106124733495



image-20241106125548211

Convolutional Neural Network Architecture

image-20241106125806206



强化学习算法library库:(集成库)

https://github.com/Denys88/rl_games

https://github.com/Domattee/gymTouch

个人github博客地址:
https://devilmaycry812839668.github.io/

前言

传统的 System.Drawing 库功能丰富,但存在平台限制,不适用于跨平台开发。.NET 8 的发布,ImageSharp 成为了一个更好的选择。

ImageSharp 是一个完全开源、高性能且跨平台的图像处理库,专为 .NET 设计。它提供丰富的图像处理功能和出色的性能,适用于桌面应用、Web 应用和云端服务。

本文将介绍 ImageSharp 的主要特点和基本用法,帮助大家快速上手这个强大的工具。

项目介绍

一款完全托管的高性能、跨平台图形库,支持在任何 .NET 6+ 环境中安装和使用。它适用于多种场景,包括设备端、云端、嵌入式和物联网等。

ImageSharp 功能丰富且使用简单,常见的操作只需几行代码即可完成。它支持多种图像格式,如 JPEG、BMP、GIF、PNG、TIFF、WebP 和 QOI 等。同时,它支持超过 25 种像素格式,并提供元数据编辑(如 IPTC、EXIF、XMP)和色彩空间转换(如 RGB、CMYK、灰度、CIELab)功能。

另外,ImageSharp 提供了超过 40 种常见的图像处理操作,性能出色且内存友好,能够处理任意尺寸的图像,包括数百万像素的大图。

项目使用

1、安装依赖库

首先需要将其添加到自己的项目中。通过NuGet包管理器安装是最简单的方式,具体如下所示:

Install-Package SixLabors.ImageSharp

或者 VS 直接搜索安装

2、加载图片

使用ImageSharp加载图像非常简单。

/// <summary>
///加载图片/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void LoadImage_Click(objectsender, RoutedEventArgs e)
{
OpenFileDialog openFileDialog
= newOpenFileDialog
{
Filter
= "Image Files|*.jpg;*.jpeg;*.png;*.bmp;*.gif"};if (openFileDialog.ShowDialog() == true)
{
using (FileStream fs = newFileStream(openFileDialog.FileName, FileMode.Open))
{
_image
= SixLabors.ImageSharp.Image.Load<Rgba32>(fs);
ImageControl.Source
=ConvertToBitmapSource(_image);
}
}
}

3、裁剪图片

使用ImageSharp调整图像大小

/// <summary>
///裁剪图片/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void CropImage_Click(objectsender, RoutedEventArgs e)
{
if (_image != null)
{
_image.Mutate(x
=> x.Crop(new SixLabors.ImageSharp.Rectangle(100, 100, 200, 200)));
ImageControl.Source
=ConvertToBitmapSource(_image);
}
}

4、旋转图片

ImageSharp也可以轻松地旋转和翻转图像。

/// <summary>
///选择图片/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void RotateImage_Click(objectsender, RoutedEventArgs e)
{
if (_image != null)
{
_image.Mutate(x
=> x.Rotate(90));
ImageControl.Source
=ConvertToBitmapSource(_image);
}
}

5、运行效果

还有更多效果和方法,大家可以自己试试。

常用方法

  • 加载和保存图像
  • Image.Load(): 加载图像文件。
  • Image.Save(): 保存图像到文件。
  • 调整图像大小
  • Resize(): 改变图像的尺寸。
  • 裁剪图像
  • Crop(): 裁剪图像到指定的矩形区域。
  • 旋转和翻转图像
  • Rotate(): 旋转图像一定的角度。
  • Flip(): 水平或垂直翻转图像。
  • 应用滤镜和效果
  • Grayscale(): 将图像转换为灰度。
  • GaussianBlur(): 应用高斯模糊效果。
  • Sharpen(): 锐化图像。
  • 调整亮度、对比度和饱和度
  • Brightness(): 调整图像的亮度。
  • Contrast(): 调整图像的对比度。
  • Saturate(): 调整图像的饱和度。
  • 绘制图像和文本
  • DrawImage(): 在图像上绘制另一幅图像。
  • DrawText(): 在图像上绘制文本。
  • 像素操作
  • ProcessPixelRows(): 提供对图像像素行的低级访问。
  • 图像格式转换
  • Image.DetectFormat(): 检测图像的格式。
  • Image.Load() 和 Image.SaveAsFormat(): 加载和保存特定格式的图像。
  • 图像元数据处理
  • Metadata: 访问和修改图像的元数据。

常用滤镜

  • Grayscale (灰度滤镜): 将图像转换为灰度,移除颜色信息,只保留亮度信息。
  • BlackWhite (黑白滤镜): 将图像转换为严格的黑白两色。
  • Sepia (棕褐色滤镜): 给图像应用一种棕褐色调,模仿旧照片的风格。
  • Invert (反转滤镜): 反转图像中的所有颜色。
  • GaussianBlur (高斯模糊滤镜): 对图像应用高斯模糊,可用于创建柔化效果或减少图像噪声。
  • Sharpen (锐化滤镜): 增强图像的边缘对比度,使图像看起来更清晰。
  • Vignette (晕影滤镜): 在图像的边缘添加暗角效果,通常用于吸引观看者的注意力到图像的中心。
  • Brightness (亮度滤镜): 调整图像的亮度。
  • Contrast (对比度滤镜): 调整图像的对比度。
  • Saturate (饱和度滤镜): 调整图像的饱和度。
  • Hue (色调滤镜): 调整图像的色调。
  • Pixelate (像素化滤镜): 将图像分割成大块的像素,创建一种像素艺术效果。
  • OilPaint (油画滤镜): 模仿油画的笔触效果。
  • Glow (发光滤镜): 在图像周围添加一层柔和的光晕。
  • BokehBlur (背景虚化滤镜): 创建一种模仿摄影中背景虚化(散景)的效果。
  • DetectEdges (边缘检测滤镜): 检测并突出显示图像中的边缘。
  • Polaroid (色调分离滤镜): 减少图像中颜色的数量,创建一种类似于复古海报的效果。

项目地址

GitHub:
https://github.com/SixLabors/ImageSharp

总结

ImageSharp 是一个功能强大的 .NET 图像处理库,提供了广泛的图像操作选项,包括加载、保存、调整大小、旋转、翻转以及应用各种滤镜和效果。

本文示例仅展示了 ImageSharp 功能的一部分,通过使用ImageSharp库大家可以在项目中轻松实现复杂的图像处理任务。

希望本文能在图像处理开发方面为各位提供有益的帮助。期待大家在评论区留言交流,分享您的宝贵经验和建议。

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!