2024年11月

前言

在工业和科研领域,环境监测系统的重要性日益凸显。上位机软件作为环境监测系统的关键组成部分,负责数据采集、处理和显示,对提高监测效率和准确性起着至关重要的作用。

本文将向大家介绍一款用 C# 开发的环境监测上位机软件。

软件介绍

本上位机软件为广西北海渔场监控项目定制的监控上位机软件。

开发环境

开发工具:Visual Studio 2019

开发框架:WinForms

使用控件:BeauGaugeInstrumentationSuitePro

主要语言:C#

主要功能

1、使用环境

  • 在数据资料目录下先安装以下控件和软件
  • 安装 BeauGaugeInstrumentationSuitePro 控件:
  • 运行BeauGaugeInstrumentationSuitePro.msi 安装包进行安装。
  • 安装上位机软件
  • 运行北部湾深海网箱养殖水质环境监控系统.msi 安装包进行安装。
  • 或者,您可以下载绿色版压缩文件,解压后直接双击运行recvdatasection.exe文件。

2、界面介绍

本上位机软件分为3个从监测节点和1个主监测节点:

  • 从节点:温度、盐度、浊度、PH值、溶解氧
  • 主节点:气温、风速、气压、俯仰角、横滚角、雨量、经纬度

同时,软件还隐藏了3个备用从节点,以备不时之需。

3、重启

软件菜单重启按钮:重新内部启动上位机软件

4. 设备管理

可以进行各种设备的管理。

点击打开和关闭则对应的相关设备进行通电和断电操作。

5、数据查看

对应监测点数据曲线图

每个监测点的数据曲线如下为例:

历史曲线图需点击如下图箭头所示按钮,弹出当天的数据信息如下所示:

6、历史
数据

可以进行各种历史数据操作,可以选择历史时间点并打开该天的数据图。

7、低信号模式

当下位机数据传输不稳定或者节点开启但无数据显示可打开低信号模式。

8、阈值设置

点击阈值设置按钮可展出阈值设置界面,再次点击隐藏,如下图:

初始化阈值设定:点击初始化阈值设定按钮打开初始阈值界面,可设定传感器数据监测范围,设定完后保存。点取消退出设定界面。初始阈值数据保存本地,即软件刚使用时设定完即可,一般不可更改。

重置:重置按钮即将初始化设定的阈值数据全部覆盖当前阈值范围数据,并关闭监测的作用。

一键开启:打开全部传感器监测数据。可手动单独开启,开启按钮提示为蓝色,关闭为白色。

若监测点数据异常在消息界面以黄色警告字体提示,同时阈值显示界面异常数据文本变红色,且每分钟自动播放2秒提示音。

项目地址

Gitee:
https://gitee.com/tytokongjian /beihai-host-computer

总结

此为北海项目中的一部分。监控上位机模板项目,应用于实际生产环境,需要下位机传感器配合使用。

希望通过本文能为大家在上位机软件开发方面提供有价值的参考。欢迎在评论区留言交流,分享您的宝贵经验和建议。

最后

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

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

inference-by-vllm.png

本文主要分享如何使用 vLLM 实现大模型推理服务。

1. 概述

大模型推理有多种方式比如

  • 最基础的 HuggingFace Transformers
  • TGI
  • vLLM
  • Triton + TensorRT-LLM
  • ...

其中,热度最高的应该就是 vLLM,性能好的同时使用也非常简单,本文就分享一下如何使用 vLLM 来启动大模型推理服务。

根据 vLLM 官方博客
vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttention
所说:

进行了 NVIDIA A10 GPU 上推理 LLaMA-7 B 和 在 NVIDIA A100 GPU(40 GB)上推理 LLaMA-13 B 两个实验,
在吞吐量上 vLLM 比最基础的 HuggingFace Transformers 高 24 倍,比 TGI 高 3.5 倍

vllm-performance.png

2.安装 vLLM

首先要准备一个 GPU 环境,可以参考这篇文章:
GPU 环境搭建指南:如何在裸机、Docker、K8s 等环境中使用 GPU

需要保证宿主机上可以正常执行
nvidia-smi
命令,就像这样:

root@test:~# nvidia-smi
Thu Jul 18 10:52:01 2024
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 525.147.05   Driver Version: 525.147.05   CUDA Version: 12.0     |
|-------------------------------+----------------------+----------------------+
| 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  NVIDIA A40          Off  | 00000000:00:07.0 Off |                    0 |
|  0%   45C    P0    88W / 300W |  40920MiB / 46068MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
|   1  NVIDIA A40          Off  | 00000000:00:08.0 Off |                    0 |
|  0%   47C    P0    92W / 300W |  40916MiB / 46068MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|    0   N/A  N/A   1847616      C   tritonserver                      480MiB |
|    0   N/A  N/A   2553571      C   python3                         40426MiB |
|    1   N/A  N/A   1847616      C   tritonserver                      476MiB |
|    1   N/A  N/A   2109313      C   python3                         40426MiB |
+-----------------------------------------------------------------------------+

安装 conda

为了避免干扰,这里使用 conda 单独创建一个 Python 虚拟环境安装 vLLM。

使用下面的命令可以快速安装最新的 miniconda,也可以去官方下载压缩包,解压并配置到 PATH 变量。

mkdir -p ~/miniconda3

wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda3/miniconda.sh

bash ~/miniconda3/miniconda.sh -b -u -p ~/miniconda3

rm -rf ~/miniconda3/miniconda.sh

然后初始化

# 根据使用的不同 shell 选择一个命令执行
~/miniconda3/bin/conda init bash
~/miniconda3/bin/conda init zsh

激活

source ~/.bashrc

创建虚拟环境安装 vLLM

创建虚拟环境并激活

conda create -n vllm_py310 python=3.10

conda activate vllm_py310

# 配置 pip 源
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple/
pip config set install.trusted-host pypi.tuna.tsinghua.edu.cn

# 在虚拟环境中安装 vllm 0.4.2 版本
pip install vllm==0.4.2

3. 准备模型

一般模型都会发布到
HuggingFace
,不过国内网络情况,推荐到
ModelScope
下载。

主流的模型 vLLM 都是支持的,具体列表可以查看官方文档:
vllm-supported-models

这里使用 Qwen1.5-1.8B-Chat 进行测试。

使用 git lfs 方式下载:

# 安装并初始化 git-lfs
apt install git-lfs -y
git lfs install

# 下载模型
git lfs clone https://www.modelscope.cn/qwen/Qwen1.5-1.8B-Chat.git

完整内容如下:

root@j99cloudvm:~/lixd/models# ls -lhS Qwen1.5-1.8B-Chat/
total 3.5G
-rw-r--r-- 1 root root 3.5G Jul 18 11:20 model.safetensors
-rw-r--r-- 1 root root 6.8M Jul 18 11:08 tokenizer.json
-rw-r--r-- 1 root root 2.7M Jul 18 11:08 vocab.json
-rw-r--r-- 1 root root 1.6M Jul 18 11:08 merges.txt
-rw-r--r-- 1 root root 7.2K Jul 18 11:08 LICENSE
-rw-r--r-- 1 root root 4.2K Jul 18 11:08 README.md
-rw-r--r-- 1 root root 1.3K Jul 18 11:08 tokenizer_config.json
-rw-r--r-- 1 root root  662 Jul 18 11:08 config.json
-rw-r--r-- 1 root root  206 Jul 18 11:08 generation_config.json
-rw-r--r-- 1 root root   51 Jul 18 11:08 configuration.json

这个目录包含了一个大模型的相关文件。以下是每个文件的作用简要说明:

  • model.safetensors
    :这是大模型的主要文件,包含了模型的权重。
  • tokenizer.json
    :这个文件包含了分词器(Tokenizer)的配置和词汇表。分词器用于将输入文本转换为模型可以处理的格式,通常是数字 ID。
  • tokenizer_config.json
    :这个文件包含了分词器的配置选项,如分词器的类型、参数设置等。
  • config.json
    :这个文件包含了模型的配置参数,定义了模型的结构和训练过程中的一些设置。它通常包括层数、隐藏单元数、激活函数等参数。
  • generation_config.json
    :这个文件包含了用于生成文本的配置参数,如生成长度、采样策略等。
  • configuration.json
    :这个文件通常是模型的额外配置文件,可能包含与模型结构或训练过程相关的配置信息。
  • vocab.json
    :这个文件包含了分词器的词汇表,通常是一个从词汇到 ID 的映射表。分词器使用这个文件来将文本中的单词转换为模型可以处理的 ID。

一般只需要注意权重文件和 tokenizer 即可。

4. 开始推理

启动推理服务

vLLM 支持提供 OpenAI 格式的 API,启动命令如下:

modelpath=/models/Qwen1.5-1.8B-Chat

# 单卡
python3 -m vllm.entrypoints.openai.api_server \
        --model $modelpath \
        --served-model-name qwen \
        --trust-remote-code

输出如下

INFO 07-18 06:42:31 llm_engine.py:100] Initializing an LLM engine (v0.4.2) with config: model='/models/Qwen1.5-1.8B-Chat', speculative_config=None, tokenizer='/models/Qwen1.5-1.8B-Chat', skip_tokenizer_init=False, tokenizer_mode=auto, revision=None, tokenizer_revision=None, trust_remote_code=True, dtype=torch.float16, max_seq_len=32768, download_dir=None, load_format=LoadFormat.AUTO, tensor_parallel_size=1, disable_custom_all_reduce=False, quantization=None, enforce_eager=False, kv_cache_dtype=auto, quantization_param_path=None, device_config=cuda, decoding_config=DecodingConfig(guided_decoding_backend='outlines'), seed=0, served_model_name=qwen)
INFO:     Started server process [614]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

vLLM 默认监听 8000 端口。

对于多卡则是增加参数
tensor-parallel-size
,将该参数设置为 GPU 数量即可,vLLM 会启动 ray cluster 将模型切分到多个 GPU 上运行,对于大模型很有用。

python3 -m vllm.entrypoints.openai.api_server \
        --model $modelpath \
        --served-model-name qwen \
        --tensor-parallel-size 8 \
        --trust-remote-code

发送测试请求

直接使用 OpenAI 格式请求

# model 就是前面启动服务时的 served-model-name 参数
curl http://localhost:8000/v1/chat/completions \
    -H "Content-Type: application/json" \
    -d '{
        "model": "qwen",
        "messages": [
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": "你是谁?"}
        ]
    }'

输出如下:

{"id":"cmpl-07f2f8c70bd44c10bba71d730e6e10a3","object":"chat.completion","created":1721284973,"model":"qwen","choices":[{"index":0,"message":{"role":"assistant","content":"我是来自阿里云的大规模语言模型,我叫通义千问。我是阿里云自主研发的超大规模语言模型,可以回答问题、创作文字,还能表达观点、撰写代码、撰写故事,还能表达观点、撰写代码、撰写故事。我被设计用来帮助用户解答问题、创作文字、表达观点、撰写代码、撰写故事,以及进行其他各种自然语言处理任务。如果您有任何问题或需要帮助,请随时告诉我,我会尽力提供支持。"},"logprobs":null,"finish_reason":"stop","stop_reason":null}],"usage":{"prompt_tokens":22,"total_tokens":121,"completion_tokens":99}}

查看 GPU 占用情况,基本跑满了

Thu Jul 18 06:45:32 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   59C    P0              69W /  70W |  12833MiB / 15360MiB |     84%      Default |
|                                         |                      |                  N/A |
+-----------------------------------------+----------------------+----------------------+
|   1  Tesla T4                       On  | 00000000:86:00.0 Off |                    0 |
| N/A   51C    P0              30W /  70W |   4849MiB / 15360MiB |      0%      Default |
|                                         |                      |                  N/A |
+-----------------------------------------+----------------------+----------------------+

+---------------------------------------------------------------------------------------+
| Processes:                                                                            |
|  GPU   GI   CI        PID   Type   Process name                            GPU Memory |
|        ID   ID                                                             Usage      |
|=======================================================================================|
|    0   N/A  N/A   3376627      C   python3                                   12818MiB |
|    1   N/A  N/A   1150487      C   /usr/bin/python3                           4846MiB |
+---------------------------------------------------------------------------------------+

5. 小结

本文主要分享如何使用 vLLM 来部署大模型推理服务, 安装好环境后,vLLM 使用非常简单,一条命令即可启动。

modelpath=/models/Qwen1.5-1.8B-Chat

# 单卡
python3 -m vllm.entrypoints.openai.api_server \
        --model $modelpath \
        --served-model-name qwen \
        --trust-remote-code


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


Torus
类在制作数学、物理或工程领域的动画时具有广泛的应用场景。

比如,通过动态演示环面的拓扑变换(如内外翻转、扭曲等),帮助我们直观地理解拓扑不变量和同胚等概念;

此外,也可以模拟磁场线在环面导体中的分布和运动,展示电磁感应现象等等。

本篇介绍Torus的主要参数和基本使用方法。

1. 主要参数

Torus
的参数不多,主要有:

参数名称 类型 说明
major_radius float 圆环面的主要半径,从环面中心到其管道中心(或称为环面中心轴)的距离
minor_radius float 环面管道的半径
resolution [int, int] 环面表面的分辨率,用于控制渲染的精细程度
u_range [float] 定义了圆环面在u方向上的参数化范围
v_range [float] 定义了圆环面在v方向上的参数化范围

如果把
Torus
圆环面看成一个轮胎的话,

那么,
major_radius
参数表示轮胎的大小,
minor_radius
参数表示轮胎的厚度。

2. 使用示例

为了有效展示
Torus
(圆环面)各个参数的使用,下面构造四个示例,

每个示例将突出
Torus
类的一个或几个关键参数,并说明这些参数如何影响环面的外观和特性。

2.1. 标准圆环面

这是一个标准的环面,其中
major_radius
决定了环面的大小,
minor_radius
决定了环面管道的厚度,


resolution
控制了环面表面的平滑度。

通过调整这些参数,可以获得不同大小和形状的环面。

torus = Torus(
    major_radius=2.5,
    minor_radius=0.5,
    resolution=(30, 30),
)

2.2. 扁平圆环面

通过限制
v_range
的值,我们可以创建一个扁平的环面。

在这个示例中,
v_range
被设置为
(0, PI/2)
,这意味着环面在
v方向
上的参数化范围被限制在一个更小的区间内,从而导致环面在视觉上变得更加扁平。

这种扁平环面可以用于模拟轮胎、甜甜圈等扁平形状的物体。

torus = Torus(
    major_radius=2.5,
    minor_radius=0.5,
    resolution=(30, 30),
    v_range=(0, PI / 2),
)

2.3. 高分辨率圆环面

增加
resolution
的值可以提高环面表面的平滑度和细节程度。

在这个示例中,
resolution
被设置为
(100, 100)
,这意味着环面在
u和v方向
上都有更高的分辨率,从而呈现出更加细腻和逼真的曲面效果。

高分辨率环面在渲染复杂场景或制作高质量动画时非常有用。

torus = Torus(
    major_radius=2.5,
    minor_radius=0.5,
    resolution=(100, 100),
)

2.4. 非标准圆环面

通过调整
u_range
的值,我们可以创建一个非标准的环面。

在这个示例中,
u_range
被设置为
(0, 3*PI/2)
,这意味着环面在
u方向
上的参数化范围被扩展到一个更大的区间内,从而导致环面在视觉上出现一部分缺失。

这种非标准环面可以用于艺术创作、数学可视化或物理模拟等领域,以展示环面在不同参数设置下的多样性和灵活性。

torus = Torus(
    major_radius=2.5,
    minor_radius=0.5,
    resolution=(30, 30),
    u_range=(0, 3 * PI / 2),
)

3. 附件

文中的代码只是关键部分的截取,完整的代码共享在网盘中(
torus.py
),

下载地址:
完整代码
(访问密码: 6872)


最近在复现 PPO 跑 MiniGrid,记录一下…

这里跑的环境是 Empty-5x5 和 8x8,都是简单环境,主要验证 PPO 实现是否正确。

01 Proximal policy Optimization(PPO)

(参考:
知乎 | Proximal Policy Optimization (PPO) 算法理解:从策略梯度开始

首先,
策略梯度方法
的梯度形式是

\[\nabla_\theta J(\theta)\approx
\frac1n \sum_{i=0}^{n-1} R(\tau_i)
\sum_{t=0}^{T-1} \nabla_\theta \log \pi_\theta(a_t|s_t)
\tag1
\]

然而,传统策略梯度方法容易一步走的太多,以至于越过了中间比较好的点(在参考知乎博客里称为 overshooting)。一个直观的想法是限制策略每次不要更新太多,比如去约束 新策略 旧策略之间的 KL 散度(公式是 plog(p/q)):

\[D_{KL}(\pi_\theta | \pi_{\theta+\Delta \theta}) = \mathbb E_{s,a}
\pi_\theta(a|s)\log\frac{\pi_\theta(a|s)}{\pi_{\theta+\Delta \theta}(a|s)} \le \epsilon
\tag2
\]

我们把这个约束进行拉格朗日松弛,将它变成一个惩罚项:

\[\Delta\theta^* = \arg\max_{\Delta\theta} J(\theta+\Delta\theta) -
\lambda [D_{KL}(\pi_\theta | \pi_{\theta+\Delta \theta})-\epsilon]
\tag3
\]

然后再使用一些数学近似技巧,可以得到自然策略梯度(NPG)算法。

NPG 算法貌似还有种种问题,比如 KL 散度的约束太紧,导致每次更新后的策略性能没有提升。我们希望每次策略更新后都带来性能提升,因此计算 新策略 旧策略之间 预期回报的差异。这里采用计算 advantage 的方式:

\[J(\pi_{\theta+\Delta\theta})=J(\pi_{\theta})+\mathbb E_{\tau\sim\pi_{\theta+\Delta\theta}}\sum_{t=0}^\infty
\gamma^tA^{\pi_{\theta}}(s_t,a_t)
\tag{4}
\]

其中优势函数(advantage)的定义是:

\[A^{\pi_{\theta}}(s_t,a_t)=\mathbb E(Q^{\pi_{\theta}}(s_t,a_t)-V^{\pi_{\theta}}(s_t))
\tag{5}
\]

在公式 (4) 中,我们计算的 advantage 是在 新策略 的期望下的。但是,在新策略下蒙特卡洛采样(rollout)来算 advantage 期望太麻烦了,因此我们在原策略下 rollout,并进行 importance sampling,假装计算的是新策略下的 advantage。这个 advantage 被称为替代优势(surrogate advantage):

\[\mathcal{L}_{\pi_{\theta}}\left(\pi_{\theta+\Delta\theta}\right) =
J\left(\pi_{\theta+\Delta\theta}\right)-J\left(\pi_{\theta}\right)\approx E_{s\sim\rho_{\pi\theta}}\frac{\pi_{\theta+\Delta\theta}(a\mid s)}{\pi_{\theta}(a\mid s)} A^{\pi_{\theta}}(s, a)
\tag6
\]

所产生的近似误差,貌似可以用两种策略之间最坏情况的 KL 散度表示:

\[J(\pi_{\theta+\Delta\theta})-J(\pi_{\theta})\geq\mathcal{L}_{\pi\theta}(\pi_{\theta+\Delta\theta})-CD_{KL}^{\max}(\pi_{\theta}||\pi_{\theta+\Delta\theta})
\tag7
\]

其中 C 是一个常数。这貌似就是 TRPO 的单调改进定理,即,如果我们改进下限 RHS,我们也会将目标 LHS 改进至少相同的量。

基于 TRPO 算法,我们可以得到 PPO 算法。PPO Penalty 跟 TRPO 比较相近:

\[\Delta\theta^{*}=\underset{\Delta\theta}{\text{argmax}}
\Big[\mathcal{L}_{\theta+\Delta\theta}(\theta+\Delta\theta)-\beta\cdot \mathcal{D}_{KL}(\pi_{\theta}\parallel\pi_{\theta+\Delta\theta})\Big]
\tag 8
\]

其中,KL 散度惩罚的 β 是启发式确定的:PPO 会设置一个目标散度
\(\delta\)
,如果最终更新的散度超过目标散度的 1.5 倍,则下一次迭代我们将加倍 β 来加重惩罚。相反,如果更新太小,我们将 β 减半,从而扩大信任域。

接下来是 PPO Clip,这貌似是目前最常用的 PPO。PPO Penalty 用 β 来惩罚策略变化,而 PPO Clip 与此不同,直接限制策略可以改变的范围。我们重新定义 surrogate advantage:

\[\begin{aligned}
\mathcal{L}_{\pi_{\theta}}^{CLIP}(\pi_{\theta_{k}}) = \mathbb E_{\tau\sim\pi_{\theta}}\bigg[\sum_{t=0}^{T}
\min\Big( & \rho_{t}(\pi_{\theta}, \pi_{\theta_{k}})A_{t}^{\pi_{\theta_{k}}},
\\
& \text{clip} (\rho_{t}(\pi_{\theta},\pi_{\theta_{k}}), 1-\epsilon, 1+\epsilon) A_{t}^{\pi_{\theta_{k}}}
\Big)\bigg]
\end{aligned}
\tag 9
\]

其中,
\(\rho_{t}\)
为重要性采样的 ratio:

\[\rho_{t}(\theta)=\frac{\pi_{\theta}(a_{t}\mid s_{t})}{\pi_{\theta_{k}}(a_{t}\mid s_{t})}
\tag{10}
\]

公式 (9) 中,min 括号里的第一项是 ratio 和 advantage 相乘,代表新策略下的 advantage;min 括号里的第二项是对 ration 进行的 clip 与 advantage 的相乘。这个 min 貌似可以限制策略变化不要太大。

02 如何复现 PPO(参考 stable baselines3 和 clean RL)

代码主要结构如下,以 stable baselines3 为例:(仅保留主要结构,相当于伪代码,不保证正确性)

import torch
import torch.nn.functional as F
import numpy as np

# 1. collect rollout
self.policy.eval()
rollout_buffer.reset()
while not done:
    actions, values, log_probs = self.policy(self._last_obs)
    new_obs, rewards, dones, infos = env.step(clipped_actions)
    rollout_buffer.add(
        self._last_obs, actions, rewards,
        self._last_episode_starts, values, log_probs,
    )
    self._last_obs = new_obs
    self._last_episode_starts = dones

with torch.no_grad():
    # Compute value for the last timestep
    values = self.policy.predict_values(obs_as_tensor(new_obs, self.device)) 

rollout_buffer.compute_returns_and_advantage(last_values=values, dones=dones)


# 2. policy optimization
for rollout_data in self.rollout_buffer.get(self.batch_size):
    actions = rollout_data.actions
    values, log_prob, entropy = self.policy.evaluate_actions(rollout_data.observations, actions)
    advantages = rollout_data.advantages
    # Normalize advantage
    if self.normalize_advantage and len(advantages) > 1:
        advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)

    # ratio between old and new policy, should be one at the first iteration
    ratio = torch.exp(log_prob - rollout_data.old_log_prob)

    # clipped surrogate loss
    policy_loss_1 = advantages * ratio
    policy_loss_2 = advantages * torch.clamp(ratio, 1 - clip_range, 1 + clip_range)
    policy_loss = -torch.min(policy_loss_1, policy_loss_2).mean()

    # Value loss using the TD(gae_lambda) target
    value_loss = F.mse_loss(rollout_data.returns, values_pred)

    # Entropy loss favor exploration
    entropy_loss = -torch.mean(entropy)

    loss = policy_loss + self.ent_coef * entropy_loss + self.vf_coef * value_loss

    # Optimization step
    self.policy.optimizer.zero_grad()
    loss.backward()
    # Clip grad norm
    torch.nn.utils.clip_grad_norm_(self.policy.parameters(), self.max_grad_norm)
    self.policy.optimizer.step()

大致流程:收集当前策略的 rollout → 计算 advantage → 策略优化。

计算 advantage 是由 rollout_buffer.compute_returns_and_advantage 函数实现的:

rb = rollout_buffer
last_gae_lam = 0
for step in reversed(range(buffer_size)):
    if step == buffer_size - 1:
        next_non_terminal = 1.0 - dones.astype(np.float32)
        next_values = last_values
    else:
        next_non_terminal = 1.0 - rb.episode_starts[step + 1]
        next_values = rb.values[step + 1]
    delta = rb.rewards[step] + gamma * next_values * next_non_terminal - rb.values[step]  # (1)
    last_gae_lam = delta + gamma * gae_lambda * next_non_terminal * last_gae_lam  # (2)
    rb.advantages[step] = last_gae_lam
rb.returns = rb.advantages + rb.values

其中,

  • (1) 行通过类似于 TD error 的形式(A = r + γV(s') - V(s)),计算当前 t 时刻的 advantage;
  • (2) 行则是把 t+1 时刻的 advantage 乘 gamma 和 gae_lambda 传递过来。

03 记录一些踩坑经历

  1. PPO 在收集 rollout 的时候,要在分布里采样,而非采用 argmax 动作,否则没有 exploration。(PPO 在分布里采样 action,这样来保证探索,而非使用 epsilon greedy 等机制;听说 epsilon greedy 机制是 value-based 方法用的)
  2. 如果 policy 网络里有(比如说)batch norm,rollout 时应该把 policy 开 eval 模式,这样就不会出错。
  3. (但是,不要加 batch norm,加 batch norm 性能就不好了。听说 RL 不能加 batch norm)
  4. minigrid 简单环境,RNN 加不加貌似都可以(?)
  5. 在算 entropy loss 的时候,要用真 entropy,从 Categorical 分布里得到的 entropy;不要用 -logprob 近似的,不然会导致策略分布 熵变得很小 炸掉。



大家好,我是汤师爷~

在当今的软件行业中,SaaS(软件即服务)模式正在迅速崛起。它改变了传统的软件使用方式。

多租户技术是SaaS模式的核心,这项技术让多个客户可以共享同一个软件系统,但每个客户的数据都是独立的,互不干扰。

本章将深入探讨多租户系统的概念、架构和实现方法。

多租户概述

什么是多租户?

多租户是SaaS(软件即服务)领域里特有的一个概念。在SaaS服务中,“租户”指的就是使用这个SaaS系统的客户。

那么租户和用户有什么区别呢?举个例子,假设你正在使用一款面向企业(ToB)的SaaS产品,"用户"通常指的是公司里实际操作这个SaaS系统的员工。而"租户"则代表整个公司或组织。也就是说,一个租户下可能包含多个用户。

多租户技术是一种软件架构,它允许多个租户共享同一个系统实例,同时确保每个租户的数据和行为互相独立、互不干扰。换句话说,尽管所有租户使用的是同一套系统,但各自的数据完全隔离,并且能够保障安全性。

传统软件模式 VS SaaS模式

传统的软件项目,一般是指软件公司根据客户的需求,专门开发一套特定的软件系统。然后,这个软件被部署在一个独立的环境里,通常就是企业内部的服务器上。

SaaS模式则不同,它将软件服务部署在云端环境中。不同的客户都能通过浏览器或网络访问,使用相同的软件服务。就好比一家自助餐厅,准备了各种各样的菜品,任何人都可以进来品尝,不需要自己下厨做饭。

多租户使用场景

对于面向企业(ToB)的SaaS产品,多租户的使用场景一般涉及到三个使用角色:企业主、员工、SaaS平台运营。

1、企业主

企业主,也就是公司的管理者,他们是SaaS平台的直接客户。企业主想要订购和使用SaaS产品,首先要在SaaS平台上创建一个“企业账号”(也就是租户)。

在这个过程中,企业主需要注册一个公司账户并填写相关信息。完成后,SaaS平台系统会为企业主创建一个专属的"租户空间"。

企业主随后可以在这个空间内订购所需的产品,并添加员工账号。一旦员工被加入到租户中,他们就能通过登录SaaS平台来使用这些产品。

2. 员工

一个员工可能会被多个企业添加,即该用户属于多个租户。这意味着员工在使用SaaS平台时,需要根据情况,切换到对应的租户。

例如,今天处理A公司的任务就切换到A公司,明天处理B公司的任务就切换到B公司。这种租户切换设计让员工能够在多个企业账号间自由切换,避免身份混淆影响使用体验。

3. SaaS平台运营

SaaS平台运营团队的主要职责是管理租户。他们需要确保每个租户的权限、资源分配、产品都得到妥善管理。

例如,当企业主新增员工时,平台必须确保该员工只能访问所属企业的数据。如果企业主想调整订购的产品,SaaS平台运营也能够迅速响应。

租户管理涉及权限控制、资源调度、能力配置、计费管理等一系列复杂操作。SaaS平台运营团队的职责就是确保这一切运转顺畅。

SaaS多租户隔离模式

SaaS多租户隔离模式

在SaaS模式下,多租户之间的资源隔离是基础且关键的一环。SaaS服务商需要在确保运营效率和控制成本的前提下,搭建一个让多个租户能够同时访问的共享环境。

虽然大家都在用同一套SaaS产品服务,但资源访问必须严格隔离,确保租户之间互不干扰。SaaS资源隔离通常包含以下几个层次:

1. 基础设施的隔离

第一层主要指SaaS系统运行所依赖的基础设施资源,比如计算资源(CPU、内存)、存储资源(数据库、文件系统)、网络资源(IP、带宽)等。

这些资源的隔离主要是为了确保不同租户在使用系统时,互相之间不会因为资源竞争而受到影响。

2. 组织权限的隔离

第二层隔离涉及组织权限的隔离,包括组织信息、用户账号、角色、权限配置、产品授权关系等。这些数据决定了谁能用什么功能,谁有操作哪些内容的权限。

组织权限隔离的目的是让每个租户拥有自己独立的组织架构和权限设置,不会因为其他租户的操作而发生冲突。

3. 业务数据的隔离

最后一层隔离涉及系统运行过程中产生的业务数据,如订单、发票、操作记录和数据报表等。通过精细的数据权限配置,这些信息可以在不同组织单元间实现完全隔离,从而保障业务信息的隐私性和安全性。

多租户架构主要解决第一层的隔离问题,即计算、存储和网络等资源的隔离。为了实现多租户隔离架构,我们需要先了解几种常见的多租户隔离模式。

竖井隔离模式

有些 SaaS 服务商选择竖井隔离模式,也就是每个租户都运行在独立的资源环境中。

有人会觉得,这不就是传统软件模式吗?为什么还是 SaaS?其实,如果这些独立资源具备标准化的租户身份识别、入驻流程、计费体系、部署和运营流程,那它仍然符合 SaaS 的定义,只不过是给每个客户都提供了一整套独立的基础设施。

优点

  • 满足强隔离需求:有些客户对系统和数据的安全性有极高要求,期望能在完全独立的环境中运行,避免与其他租户的应用实例或数据混合。
  • 计费逻辑简单:对 SaaS 服务商来说,资源使用计费可能很复杂,尤其是涉及计算、存储和网络资源的场景。但在竖井模式下,每个租户都是独立环境,计费模式相对来说会简单许多。
  • 降低故障影响面:因为每个客户的系统都是独立的,一个环境出问题不会影响其他客户的使用体验。

缺点

  • 规模化困难:每新增一个租户就要建立一套独立的环境,少量租户还能应付,但面对成千上万的租户,管理和运营这些环境的难度会成倍增加。
  • 成本高:每个租户单独的环境让机器成本上升,导致 SaaS 服务的盈利能力大打折扣。
  • 敏捷迭代受限:SaaS 的优势之一是快速迭代响应市场需求,但竖井隔离模式让这点变得不易操作,因为更新和管理每个租户的独立环境非常耗时、复杂。
  • 系统管理和监控复杂:在同一个环境中管理和监控基础设施相对简单,但每个租户都独立后,这种分散模式下的管理和监控会变得极具挑战性。

共享模式

相信很多 SaaS 服务商会优先选择共享模式,也就是多租户共享一套基础设施资源,这样能让 SaaS 软件服务更加高效、敏捷、低成本。

优势

  • 高效管理:在共享模式下,可以集中管理和运营所有租户,极大提升管理效率。同时,基础设施的配置管理和监控也更加简单。相比竖井模式,共享模式下的产品迭代更新速度更快。
  • 成本低:SaaS 服务商的成本中,基础设施占很大比例。在共享模式下,服务商可以根据租户的实际资源使用情况动态调整系统,极大提高基础设施的利用率,从而降低整体成本。

劣势

  • 租户相互影响:因为所有租户共享同一套资源,如果有租户大量占用资源,可能会影响其他租户的使用体验。为了解决这一问题,通常需要在技术架构上设计一些限制措施(如限流、降级、服务器隔离等),以控制影响范围。
  • 租户计费困难:在竖井模式下,很容易统计单个租户的资源消耗。而在共享模式中,由于所有租户共用资源,准确计算每个租户的使用成本会更加复杂,需要更多的精力和技术投入来实现合理的计费。

分域隔离模式

传统大企业往往更青睐私有化部署和个性化交付的传统模式,因为它们需要更强的管控能力和更高的安全性。而中小企业因为预算有限,需求通常也更标准化,因此更倾向于选择价格低、订购方便的 SaaS 产品。

为满足不同客户的需求,还有一种融合了竖井模式和共享模式的分域隔离模式。

在这种模式下,将资源细分为基础域和专用域:基础域使用共享模式,所有租户共享一套资源;专用域则采用竖井模式,每个租户拥有独立的资源环境。

大多数中小客户通常在基础域中使用 SaaS 产品,而只有少数具备强付费能力并有强隔离需求的大客户会选择在专用域中运行 SaaS 产品。

需要注意的是,为避免产生多个产品版本,SaaS 服务商需确保基础域和专用域的产品版本保持一致。

个性化需求尽可能通过 PaaS 平台来实现,让 ISV(独立软件开发商)参与建设。否则,一旦 SaaS 产品的标准化程度降低,多版本的维护将变得极其困难。

多租户系统的定位

在了解了多租户的使用场景,以及各种多租户隔离模式后,我们可以总结一下多租户系统的定位:

多租户系统的核心目的是让多个企业用户共享一套SaaS产品,但同时确保企业之间的数据和行为是完全隔离的。通过这种设计,系统能够在满足不同用户需求的同时,提供灵活的资源配置和高效的管理方式。

根据不同的用户需求,多租户系统可以在资源上做出调整,灵活支持资源的共享或隔离。例如,对于注重成本的客户,可以采用共享的资源模式;而对于数据敏感度高的客户,则可以采用竖井隔离等独立的资源分配方式。

这种灵活性让SaaS服务商可以在资源效率和客户个性化需求之间找到平衡,不论是降低成本,还是满足高安全需求,都有相应的方案。多租户系统需要具备的能力包括:

  • 支持多个租户共享同一套云资源,如计算、存储和网络资源等。同时,也支持单个租户独享一套云资源。
  • 实现租户之间的数据和行为隔离,并能够对租户进行分权分域控制。
  • 支持租户内部的组织架构管理,方便对产品进行授权和管理。
  • 根据客户需求,不同的产品功能可选择运行在不同的云资源上。

多租户概念模型

我们已经了解了多租户系统的定位和它需要具备的能力。现在,让我们深入探讨一下多租户系统的概念模型。概念模型是多租户系统的"骨架",帮助我们理解系统各部分的组织和运作方式。

多租户的核心概念模型

  • 租户
    :通常指一个企业客户,不同租户之间的数据和行为是相互隔离的。
  • 用户
    :某个租户内的具体使用者,使用账号、密码等信息登录到 SaaS 系统,使用软件服务。
  • 组织
    :如果租户是企业客户,通常会有自己的组织架构。
  • 员工
    :指组织内部的具体成员。
  • 解决方案
    :为解决客户的特定业务问题,SaaS 服务商将产品与服务组合打包,提供整体解决方案。
  • 产品
    :SaaS 服务商售卖给客户的软件应用,能够帮助客户实现端到端流程的闭环解决方案。
  • 资源域
    :用于运行一个或多个软件应用的一套云资源环境。
  • 云资源
    :SaaS 产品一般部署在各类云平台上(如阿里云、腾讯云、华为云等),这些平台提供的计算、存储、网络、容器等资源,被抽象为云资源。

SaaS平台可以创建和管理多个平台用户、租户和资源域,这样的模型结构,可以让多个企业客户和用户能在同一平台上运行,而彼此之间的数据和资源独立。

一个平台用户可以关联到多个租户。例如,张三作为一个平台用户,可以同时属于租户A和租户B。这种设计让平台用户能够灵活切换所属租户,方便在不同企业账号间工作。

在单个租户中,也可以拥有多个用户,让企业内多名员工共享租户的资源和服务。

单个租户可以订购多个解决方案,一个解决方案往往包含多个产品,以满足企业客户的多样需求。这些产品可以运行在特定的资源域,保证不同产品在同一租户中的高效管理和资源隔离。

租户内的组织架构支持上下级关系,在单个组织单元内,可以配置多个员工,并将员工与平台用户绑定,便于员工通过自己的账户访问相关服务。

概念模型的应用场景示例

1、租户与内部模型关系

在SaaS产品中,租户是最顶层的概念,可以理解为一个大房子的租赁人,而房子内部的组织、用户、产品、云资源等模型就像是这个房子里的各种家具和设施。

换句话说,租户是SaaS产品为每个客户或企业专门划分出的独立空间,而组织、用户、产品、云资源等模型则是租户内部的细分结构,为不同的使用需求和权限分配提供支持。

  • 租户:相当于整套房子的所有权,即租户拥有这套房子在SaaS平台中的使用权,是所有内部资源的顶层管理者。
  • 组织:类似房间的布局,每个房间有特定的功能(比如子公司、部门),组织架构帮助企业在平台内映射现实中的管理层级关系。
  • 用户:就像在房子里活动的人员,用户被赋予不同的角色和权限,决定了谁可以进入哪些房间,谁可以使用哪些家具。
  • 产品:如同家里各种各样的家具、电器、设施,满足不同的需求。不同租户可以选择不同的产品组合,随需增加或减少,来满足他们的业务需求。
  • 云资源:类似于水电煤等基础设施,支持房子里的各项功能正常运行,确保产品稳定、流畅地提供服务。

通过这种类比可以看出,SaaS产品将租户作为最顶层的概念,为企业提供了一套独立空间,租户内部的各项资源则在这个框架下被灵活管理和使用,让企业客户可以获得定制化的服务和资源隔离的安全保障。

2、租户身份识别

在多租户SaaS系统中,无论采用哪种隔离模式,准确识别租户身份并获取相应资源配置是非常关键的。

当用户登录SaaS系统后,系统会生成并返回租户上下文信息。这个上下文信息包括用户关联的租户和对应的隔离模式(如共享或独立资源)。

租户上下文信息会附加在每次系统交互中,贯穿整个系统调用链。从用户请求到系统内部处理的每一步,都保留了租户上下文。这样,上游的请求处理模块就可以知道如何路由和访问下游资源。

租户上下文信息让系统能够在请求传递过程中精准识别租户身份和配置。系统会根据上下文动态选择数据库、应用实例或网络资源,实现数据和资源隔离,确保不同租户的访问互不干扰。

3、租户计费计量管理

租户的计费计量管理,是SaaS平台不可或缺的一部分。不同的隔离模式下,计费方式有所不同。

竖井隔离模式下,计费相对简单。因为每个租户使用的资源(计算、存储、网络等)都是单独分配的,就像每个租户有自己的一块“独立地盘”。

因此,我们只需统计每个租户占用的资源量,就能计算出费用。这种模式逻辑清晰,一目了然。

共享模式下,计费则比较复杂。因为多个租户共享同一资源池,理想情况是,确保每个租户只为自己实际用到的部分买单。

通常,我们会综合考量几个指标,比如请求的数量、存储容量、创建的数据对象数量等。通过这些数据的组合,我们可以相对准确算出每个租户的费用。

多租户系统应用架构

我们刚刚探讨了多租户系统的概念模型和关键设计。现在,让我们看下多租户系统的应用架构是如何从一层层构建起来的。

1、应用层设计

应用层的主要作用是为具体的用户场景提供应用服务,帮助用户在特定场景下完成操作。通过编排领域层的各项能力,实现SaaS产品的核心功能。应用层包含两个关键模块:

  • 租户运营平台模块
    :这个模块负责SaaS平台的整体运营管理,包含客户管理、租户管理、云资源管理、订单管理、平台用户管理和数据分析等功能。它就像是平台的“指挥中心”,保证租户运营顺畅,帮助平台方掌控整体业务。
  • 商家后台模块
    :这个模块主要为企业客户服务,帮助他们自主管理相关功能。包括能力订购管理、费用账单管理、续费管理和能力授权管理。商家后台让客户在平台上方便管理自己订购的服务,并能随时查看账单和费用情况。
  • 开放平台模块
    :该模块为第三方开发者或合作伙伴提供访问和集成接口的能力。

2、领域层设计

领域层的核心是围绕核心业务对象,提供可复用的业务能力,这是对系统整体复用和抽象的结果。它包含以下模块:

  • 租户管理模块
    :负责租户的入驻管理,包括租户的资质认证与审核,租户隔离模式配置等。这个模块确保新租户顺利入驻,并设置合适的资源隔离模式。为租户提供一个安全、独立的使用环境。
  • 产品管理模块
    :负责产品版本配置、产品能力管理、产品生命周期管理等。
  • 云资源管理模块
    :云资源的管理、资源池管理、弹性伸缩策略等都在这个模块中实现。它为平台提供强大的资源管理能力,确保资源配置合理。
  • 计费计量管理模块
    :涉及计费方案管理、订购管理、履约、续费和账单报表。这个模块是SaaS平台收益的关键,确保平台能够精准收取租户的费用,并且账单清晰,续费顺畅。
  • 运营管理模块
    :包括客户运营、营销管理、订单管理和数据报表。该模块为平台提供全面的运营支撑,帮助平台提升客户体验、优化营销策略,保持订单和数据分析的良好运作。

本文已收录于,我的技术网站:
tangshiye.cn
里面有,算法Leetcode详解,面试八股文、BAT面试真题、简历模版、架构设计,等经验分享。