2024年8月

Spherical Voxelization

  • 标签: voxelization
  • AI 摘要: 文档介绍了球面体素化的过程,包括重要的类和方法,如ConvertToSphericalVoxel和spherical_voxel_optimized,详细解释了参数及其作用。球面体素化通过将点云转换为球面坐标系,利用自适应采样权重来平衡不同纬度区域的点密度,从而有效捕捉几何特征。文中还提到C++绑定的sv.compute函数,负责体素特征的计算与填充,确保在特征计算中考虑邻近体素的信息。
  • 最相关链接:
    https://github.com/CVLAB-Unibo/compass

Spherical Voxelization

参考链接:

代码组成:

ConvertToSphericalVoxel类:最高接口,实例化一个converter类,调用convert转换局部点云
↓
spherical_voxel_optimized方法:在convert中调用,实现转换,先转换到球面坐标系,然后进行体素化
↓
spherical_voxel.compute方法:最终实现体素化,用pybind绑定C++代码最终实现

ConvertToSphericalVoxel

from utils import geometry as ug

class ConvertToSphericalVoxel():

    """
    Convert point cloud to spherical voxel [beta = 2 * bandwidth,  alfa = 2 * bandwidth, num_radial_division].
    Alfa in [0, 2pi], Beta in [0, pi]
    """

    def __init__(self, bandwidth, radius_support, num_radial_division, num_points, random_sampling):

        self.bandwidth = bandwidth
        self.radius_support = radius_support
        self.num_radial_division = num_radial_division
        self.num_points = num_points
        self.random_sampling = random_sampling

    def __call__(self, point_cloud):

        features, pts_normed = ug.spherical_voxel_optimized(points=point_cloud,
                                                size_bandwidth=self.bandwidth,
                                                size_radial_divisions=self.num_radial_division,
                                                radius_support=self.radius_support,
                                                do_random_sampling=self.random_sampling,
                                                num_random_points=self.num_points)

        return features, pts_normed
……

参数解释:

  • bandwidth: 球面体素化的带宽,通常用于定义球面信号的分辨率。它决定了角度方向上的采样密度(球面坐标系的
    \(\beta\)

    \(\alpha\)
    ),影响了球面信号的分辨率,PRIN, LMVD, Compess等设置为24。
  • radius_support: 支持半径,定义了local patch的支持半径,也就是说它确定了从关键点向外延伸的范围内哪些点将被纳入local patch。
  • num_radial_division: 表示径向(从关键点向外辐射的方向)上的分割数目。它影响了在径向方向上球面信号的分辨率。
  • num_points: 采样点的数量,这个值与local patch的固定点数一致(即1024点),确保输入到转换过程中的点数是一致的,这对于后续处理和模型输入非常重要。
  • random_sampling: 控制是否在从点云中选择点时进行随机采样,设置为
    True
    使得在局部区域内的点采样更加多样化,避免由于局部密度过高或过低而导致的信息丢失。随机采样可以让网络更具鲁棒性,适应不同点云的分布。

spherical_voxel_optimized

def spherical_voxel_optimized(points: np.ndarray, size_bandwidth: int, size_radial_divisions: int,
                              radius_support: float, do_random_sampling: bool, num_random_points: int) \
        -> Tuple[np.ndarray, np.ndarray]:
    """Compute spherical voxel using the C++ code.

    Compute Spherical Voxel signal as defined in:
    Pointwise Rotation-Invariant Network withAdaptive Sampling and 3D Spherical Voxel Convolution.
    Yang You, Yujing Lou, Qi Liu, Yu-Wing Tai, Weiming Wang, Lizhuang Ma and Cewu Lu.
    AAAI 2020.

    :param points: the points to convert.
    :param size_bandwidth: alpha and beta bandwidth.
    :param size_radial_divisions: the number of bins along radial dimension.
    :param radius_support: the radius used to compute the points in the support.
    :param do_random_sampling: if true a subset of random points will be used to compute the spherical voxel.
    :param num_random_points: the number of points to keep if do_random_sampling is true.

    :return: A tuple containing:
        The spherical voxel, shape(size_radial_divisions, 2 * size_bandwidth, 2 * size_bandwidth).
        The points used to compute the signal normalized according the the farthest point.
    """
    if do_random_sampling:
        min_limit = 1 if points.shape[0] > 1 else 0
        indices_random = np.random.randint(min_limit, points.shape[0], num_random_points)
        points = points[indices_random]

    pts_norm = np.linalg.norm(points, axis=1)
    # Scale points to fit unit sphere
    pts_normed = points / pts_norm[:, None]
    pts_normed = np.clip(pts_normed, -1, 1)

    pts_s2_coord = S2.change_coordinates(pts_normed, p_from='C', p_to='S')
    # Convert to spherical voxel indices
    pts_s2_coord[:, 0] *= 2 * size_bandwidth / np.pi  # [0, pi]
    pts_s2_coord[:, 1] *= size_bandwidth / np.pi # raw 2*size_bandwidth/2*np.pi
    pts_s2_coord[:, 1][pts_s2_coord[:, 1] < 0] += 2 * size_bandwidth

    # Adaptive sampling factor sin{pi*[(1/2,..., 2*size_bandwidth+1/2)/(2*size_bandwidth)]}
    # 能更好的聚合点云信息,但是也会导致更多的形变,有得必有失
    daas_weights = np.sin(np.pi * (2 * np.arange(2 * size_bandwidth) + 1) / 4 / size_bandwidth).astype(np.float32)
    voxel = np.asarray(sv.compute(pts_on_s2=pts_s2_coord,
                                  pts_norm=pts_norm,
                                  size_bandwidth=size_bandwidth,
                                  size_radial_divisions=size_radial_divisions,
                                  radius_support=radius_support,
                                  daas_weights=daas_weights))
    pts_normed = points / np.max(pts_norm)
    return voxel.astype(np.float32), pts_normed.astype(np.float32)
  • pts_norm
    是local patch的点云径向距离,所以
    local patch输入的时候最好经过对于关键点的中心化操作
    ,不然径向距离会是关于坐标系原点的。
  • S2.change_coordinates
    用于将点云从笛卡尔坐标系转换成球面坐标系,球面坐标系解释见WIKI,简单来说就是两个坐标,维度角度坐标\beta,和经度角度坐标\alpha
  • daas_weights是自适应权重:
    • 采样密度平衡
      :在球面坐标系中,由于纬度(通常用
      β
      表示)不同区域的面积差异,不同区域的点密度会有所不同。例如,在球面的极地区域(纬度接近
      0

      π
      的区域),同样的角度变化可能覆盖的球面面积较小,而在赤道区域,面积较大。为了避免在这些区域中出现过度或不足的采样,自适应采样权重用于平衡不同纬度区域的影响。
    • 信息保持
      :通过在不同的纬度上使用不同的采样权重,可以更精确地保留球面上重要的几何特征,特别是在特定的关键区域。这样可以确保球面信号在高纬度和低纬度区域都能有效地捕捉到有意义的几何信息。
  • sv.compute
    用于体素转换。

sv.compute

该函数是用pybind绑定的C++方法,文件为
spherical_voxel.cc
,代码解释如下:

初始化

    const float interval = radius_support / (size_radial_divisions);
    std::vector<std::vector<std::vector<std::vector<std::vector<float> > > > > grids;
    std::vector<std::vector<std::vector<float> > > features;
   
    grids.resize(size_radial_divisions);
    features.resize(size_radial_divisions);
  
    for (auto &beta: grids) {
	      beta.resize(2 * size_bandwidth);
	      for (auto &alpha: beta) {
	          alpha.resize(2 * size_bandwidth);
	      }
    }

    for (auto &beta: features) {
        beta.resize(2 * size_bandwidth);
        for (auto &alpha: beta) {
            alpha.resize(2 * size_bandwidth, 0);
        }
    }
  • interval表示径向分割下每个体素的径向长度
  • grids用来存储每个体素覆盖的所有点,可以通过下面的初始化看到,会初始化径向,维度,经度,每个体素是一种voxel
  • feature用来存储最终每个体素的特征(特征是密度特征)

grids填充

    // mapping the points to the voxel grid
    for (size_t i = 0; i < pts_on_s2.size(); i++) {
        int r_idx = int(pts_norm[i] / interval);
        // except for the points radius larger than radius_support
        if (r_idx > size_radial_divisions - 1) r_idx = size_radial_divisions - 1; 

        int beta_idx = int(pts_on_s2[i][0] + 0.5f);
        if (beta_idx > 2 * size_bandwidth - 1) beta_idx = 2 * size_bandwidth - 1;

        int alpha_idx = int(pts_on_s2[i][1] + 0.5f);
        if (alpha_idx > 2 * size_bandwidth - 1) alpha_idx = 2 * size_bandwidth - 1;

        grids[r_idx][beta_idx][alpha_idx].emplace_back(std::vector<float>{pts_norm[i], pts_on_s2[i][0], pts_on_s2[i][1]});
    }

这里会遍历每个点,计算每个点的径向体素所用
r_idx
,纬度体素索引
beta_idx
,经度体素索引
alpha_idx
,然后push到对应的体素里面。

feature计算

首先计算每个体素的经度左右特征计算边界
left

right
(也就是说每个体素的特征计算并不仅仅只考虑本体素内部,还有一些可能出现的相邻体素),这里计算左右边界就用到自适应权重,维度高的,左右边界会宽一些。

之后根据左右边界访问对应体素,并取出体素中所有点,基于径向距离确定点是否靠近本体素中心,越靠近该点的特征权重越大([0, 1])。

然后考虑径向相邻体素内部的点,用于本体素的特征计算,因为从径向考虑,点分布相对连续,需要补充这样的信息。

最后计算本体素的特征(密度特征(加过权的点个数))

    // compute the feature of each voxel
    for (size_t i = 0; i < size_radial_divisions; i++) {
        for (size_t j = 0; j < 2 * size_bandwidth; j++) {
            for (size_t k = 0; k < 2 * size_bandwidth; k++) {
                const float left = std::max(0.f, k - 0.5f / daas_weights[j]);
                const float right = std::min(2.f * size_bandwidth, k + 0.5f / daas_weights[j]);
                float sum = 0.f;
                int cnt = 0;

                for (int m = int(left + 0.5f); m < int(right + 0.5f); m++) {
                    for (int n = 0; n < grids[i][j][m].size(); n++) {
                        if (grids[i][j][m][n][2] > left && grids[i][j][m][n][2] < right) {
                            sum += 1.f - std::abs(grids[i][j][m][n][0] / interval - (i + 1)); // radial feature weight
                            cnt++;
                        }
                    }
                    
                    // 在实际情况中,点云数据可能分布在两个相邻的径向分割之间,
                    // 尤其是当点的径向距离位于两个径向分割的边界附近时。
                    // 为了防止因单纯考虑当前径向分割而导致信息的丢失,
                    // 代码会查找相邻径向分割中满足条件的点,并将它们的贡献也加到当前体素单元的特征值中。
                    if (i < size_radial_divisions - 1) {
                        for (int n = 0; n < grids[i + 1][j][m].size(); n++) {
                            if (grids[i + 1][j][m][n][2] > left && grids[i + 1][j][m][n][2] < right) {
                                sum += 1.f - std::abs(grids[i + 1][j][m][n][0] / interval - (i + 1));
                                cnt++;
                            }
                        }
                    }
                }

                // 与径向分割不同,纬度分割(即 beta 方向)代表的是球面坐标中的角度,
                // 分割的区域代表不同的“环”或“带”。
                // 在这种情况下,每个纬度分割对应的球面区域是明确的,
                // 且这些分割区域之间没有交叉,因此点不会“跨越”到另一个纬度分割。

                if (cnt > 0) {
                    features[i][j][k] = sum / cnt;
                }
            }
        }
    }

今天 12:24 开始收到阿里云的电话、短信与邮件通知,博客站点的其中一台负载均衡因 DDoS 攻击被阿里云屏蔽

您的IP: x.x.x.x 实例名称:yy 受到攻击,攻击流量已超过DDoS基础防护的黑洞阈值,服务器的所有公网访问已被屏蔽,屏蔽时长20分钟,屏蔽时间内未再次被攻击将自动解除否则会延期解除。详情请登录流量安全控制台,事件中心查看。黑洞状态无法人工解除,请耐心等待系统自动解封。

博客站点针对不同线路部署了多台负载均衡,但攻击者对此了如指掌,我们连躲的机会都没有,紧接着其他负载均衡一台一台被 DDoS 攻击被阿里云屏蔽,与去年10月2日遇到的那波 DDoS 攻击差不多,详见
【故障公告】遭遇用心良苦的疯狂攻击:DDoS + CC攻击

非常抱歉,这次大规模 DDoS 攻击给大家带来了麻烦,请大家谅解。

困境中的园子现在买不起天价的高防,只能等着攻击过去,望大家理解。

截止13:40,攻击还在一波一波地继续。

14:05,阿里云解除了全部屏蔽,攻击没再接续。

公众号同步发布:
https://mp.weixin.qq.com/s/hEwh0zzTIIJtcHIs8AR2fQ

最长公共子序列

  • 本文讲解的题与leetcode1143.最长公共子序列这题一样,阅读完可以挑战一下。

力扣题目链接

题目叙述:

给定两个字符串,输出其最长公共子序列,并输出它的长度

输入:

ADABEC和DBDCA

输出:

DBC
3

解释

最长公共子序列是DBC,其长度为3

动态规划思路:

  • 我们这题先构建一个模型,我们使用两个指针
    i
    ,
    j
    ,分别用于遍历a字符串,b字符串。如图所示:

img

  • 然后我们可以设想一个状态变量,也就是一个函数。一个关于两个变量相关的函数,这在代码中体现为二维数组
    f

  • 然后
    f[i][j]
    表示什么呢?表示序列
    a[1,2,3....i]

    b[1,2,3....j]
    的最长公共子序列的长度

状态变量的含义

  • 在这里的状态变量为
    f[i][j]
    ,它的含义是a的前i个字符与b的前j个字符的最长公共子序列的长度

  • 现在就要观察
    a[i]
    ,
    b[j]
    是否在当前的最长公共子序列当中。

  • 具体情况如下图:

img

递推公式:

  • f[i][j]
    可以分为三种情况讨论,就是:
  1. a[i]

    b[j]
    都在最长公共子序列当中,也就是
    a[i]==b[j]
  2. a[i]!=b[j]
    ,并且
    a[i]
    不在公共子序列当中。
  3. a[i]!=b[j]
    ,并且
    b[j]
    不在公共子序列当中。
  • 那我们的递推公式就可与分为两种情况:
    • f[i][j]=f[i-1][j-1]+1

      a[i]==b[j]
    • f[i][j]=max(f[i-1][j],f[i][j-1])

      a[i]!=b[j]
  • 显而易见,我们的边界条件为:
    • f[0][j]=0
    • f[i][0]=0
//m是a字符串的长度,n是b字符串的长度
for(int i=1;i<=m;i++){
    for(int j=1;j<=n;j++){
        //因为我们的f数组是从下标1开始,而字符串是从0开始的下标
        if(a[i-1]==b[j-1]) f[i][j]=f[i-1][j-1]+1;
        else f[i][j]=max(f[i-1][j],f[i][j-1]);
    }
}

遍历顺序

  • 经过上面的分析,明显遍历顺序为
    i从小到大

    j也是从小到大

初始化

  • 初始化边界为0即可

举例打印dp数组

  • 如图所示

如何找出对应的最长公共子序列的长度

  • 我们使用p数组来记录每一次
    f[i][j]
    的值来源于哪一个方向


    • 1方向代表左上方
    • 2方向代表左方
    • 3方向代表上方
  • 代码改造如下:

for(int i=1;i<=m;i++){
    for(int j=1;j<=n;j++){
        if(a[i-1]==b[j-1]){
            f[i][j]=f[i-1][j-1]+1;
            //左上方
            p[i][j]=1;
        }
        else if(f[i-1][j]>f[i][j-1]){
            f[i][j]=f[i][j-1];
   			//左边
            p[i][j]=2;
        }
        else{
            f[i][j]=f[i-1][j];
			//上边
            p[i][j]=3;
        }
    }
}
  • p[i][j]
    代表前驱的位置。

算法的执行过程

  • 我们要找到最长公共子序列,只需要找到从结尾开始,往前找到
    p[i][j]==1
    ,也就是来源于左上方的哪些元素的集合,就是我们的最长公共子序列。(并不是棋盘中所有
    p[i][j]==1
    )的元素,而是从右下角出发,往回找到的所有
    p[i][j]==1
    的那些元素。
  • 例子如下:

img

  • 我们使用
    s数组
    来储存最长公共子序列

  • 代码实现:

int i,j,k;
char s[200];
i=m;j=n;k=f[m][n];
while(i>0&&j>0){
    //左上方
    if(p[i][j]==1){
        s[k--]=a[i-1];
        i--;j--;
    }
    //左边
    else if(p[i][j]==2) j--;
    //上边
    else i--;
}
for(int i=1;i<=f[m][n];i++) cout<<s[i];

最终代码实现:

#include <iostream>
#include <cstring>
using namespace std;

char a[200];
char b[200];
int f[205][205];
int p[205][205];
int m, n;

void LCS() {
	int i, j;
	m = strlen(a);
	n = strlen(b);

	for (i = 1; i <= m; i++) {
		for (j = 1; j <= n; j++) {
			if (a[i - 1] == b[j - 1]) {
				f[i][j] = f[i - 1][j - 1] + 1;
				p[i][j] = 1; 
			}
			else if (f[i - 1][j] > f[i][j - 1]) {
				f[i][j] = f[i - 1][j];
				p[i][j] = 2; 
			}
			else {
				f[i][j] = f[i][j - 1];
				p[i][j] = 3; 
			}
		}
	}
	cout << f[m][n] << endl;
}
//寻找出当初的最长公共子序列。
void getLCS() {
	int i = m, j = n, k = f[m][n];
	char s[200];
	s[k] = '\0'; 
	while (i > 0 && j > 0) {
		if (p[i][j] == 1) {
			s[--k] = a[i - 1];
			i--; j--;
		}
		else if (p[i][j] == 2) {
			i--;
		}
		else {
			j--;
		}
	}

	cout << s << endl;
}

int main() {
	cin >> a >> b;
	LCS();
	getLCS();
	return 0;
}

在大模型的开发与应用中,数据预处理、模型开发、训练和推理构成四个关键环节。本文将重点探讨推理环节。在之前的博客中,社区用户
BentoML

贝壳
的案例提到了使用 JuiceFS 社区版来提高模型加载的效率。
本文将结合我们的实际经验,详细介绍企业版在此场景下的优势

下图是一个典型的大模型推理服务的架构。我们可以观察到几个关键特点。首先,架构跨越多个云服务或多个数据中心。目前在大模型领域, GPU 资源紧张,多数厂商或公司倾向于采用多云、多数据中心或混合云的策略来部署他们的推理服务。

另一个特点是,为了确保数据一致性和管理的便捷性,会在特定地区选择公有云的对象存储作为所有模型数据的存储点。当进行推理任务调度时,可能会选取特定云服务进行任务调度。数据模型的拉取过程需要人工介入,如提前进行数据拷贝。这是因为调度系统不清楚当前数据中心具体需要哪些数据,而这些数据又是动态变化的,所以数据拷贝过程会带来额外成本。

此外,从每个推理计算集群的内部情况来看,由于是规模庞大的集群,会有数百到数千 GPU 卡,因此在推理服务器初始化时,会有高并发模型数据拉取需求。

因此,概括地说在大模型推理与存储相关的挑战主要集中这样几个方面:高效访问数据、跨区域数据快速分发、存量数据读取以及资源优化
。接下来将逐个为大家介绍我们在这些场景中的实践经验。

挑战 1:如何保证大模型数据的高吞吐、高并发读取?

推理环节常需处理百 GB 级别的模型文件,满足高并发顺序读取需求。加载速度是用户最关注的问题之一。
为了满足这种场景的性能需求,可以借助 JuiceFS 企业版的分布式缓存构建大规模的缓存空间。将常用模型数据集中存储在缓存集群中,能显著提高数据读取速度,特别是在同时启动数千个推理实例时。此外,对于需要频繁切换模型的 AI 应用场景,如 Stable Diffusion 文生图服务,缓存集群可以大幅减少模型加载时间,从而直接提升用户体验。

例如在单机单卡加载 Safetensors 格式的 Stable Diffusion 模型时,从缓存集群读取数据的延迟可低至 0.5ms,而从对象存储读取的延迟通常在 20ms 左右, 性能提升了将近 40 倍

下图是 JuiceFS 分布式缓存的架构图,上层为推理集群,中间层为 JuiceFS 缓存集群,底层为对象存储,右上角是元数据服务。在推理服务部署后,首先通过推理集群上挂载的 JuiceFS 访问所需的模型数据。如果数据可以在推理集群的本地内存缓存中找到,则直接使用;若未命中,则查询位于中间的缓存集群。缓存集群如果也未命中,最后会从对象存储读取数据。

虽然推理集群和缓存层从图上看似乎是分开的两个层次,但在实际应用或部署中,如果GPU 机器上有 NVMe SSD,这两层可以合并。

在每个 GPU 机器都配备多块 SSD 的情况下,下图示例中,每个 GPU 机器配有三块 SSD,其中一块 SSD 用作本地缓存,其余两块 SSD 则用作分布式缓存的存储盘。
这种情况下,我们推荐一个部署方式:在一个 GPU 服务器上部署两个客户端,FUSE daemon 和缓存集群客户端
。当推理任务需要读取数据时,它首先会尝试从本地 FUSE 挂载点读取数据。如果本地缓存中没有相应的模型数据,推理任务将通过同一台机器上的另一个 JuiceFS 客户端访问分布式缓存。完成数据读取后,数据将返回给推理任务,并在缓存集群管理的两块 SSD 及本地 FUSE 挂载点上缓存,以便未来快速访问。

这种在一个 GPU 服务器上部署两个客户端的做法有两个主要好处:

  • 首先,通过本地缓存,可以尽量减少网络通信的开销,虽然 GPU 服务器间通过高速网卡进行网络通信,但网络通信本身还是会产生大量的开销;
  • 其次,通过缓存集群客户端,可以让推理任务访问其它 GPU 服务器上的数据,实现一个分布式缓存集群的效果。

挑战 2:如何在多云、混合云架构中有效地分发模型数据到各计算节点?

在多云和混合云架构中,由于数据分散在不同的云平台和数据中心,传统的手动介入、拷贝和迁移方法不仅成本高,而且管理和维护也较为复杂,包括权限控制在内的各种问题都十分棘手。

JuiceFS 企业版镜像文件系统功能允许用户将数据从一个地区复制到多个地区,形成一对多的复制关系
。整个复制流程对用户和应用来说是透明的:只需将数据写入指定区域,系统便会自动规划并复制到其它多个区域。

下图展示了在镜像文件系统中数据写入与数据读取时的流程。图中展示了两个区域:源区域和镜像区域。当数据在源区域写入时,JuiceFS 会自动将数据从源区域复制到镜像区域。

在读取数据时,镜像区域的客户端首先尝试从其所在区域的对象存储中拉取数据。如果数据不存在或因同步延迟未到达,则自动回退到源区域存储,通过备用数据源链路拉取数据。因此,镜像区域的所有客户端最终都能访问到数据,虽然部分数据可能来自备用数据源。

写数据流程示例

这里展示了一个大模型企业实际部署镜像文件系统的案例,其架构与文章开头展示的典型架构图相似。在图的顶部有一个中心集群,该集群作为数据生产的源头。

  • 步骤 1:写数据
    。数据首先在中心集群中被创建并写入;
  • 步骤 2:全量镜像元数据
    。数据生产完成后,将写入到 JuiceFS 中,触发元数据的全量镜像流程。如图所示,数据从中心的 JuiceFS 元数据服务被镜像到一个或多个边缘集群(本例中为三个),使得边缘集群能够就近访问本地集群内的元数据;
  • 步骤 3:预热缓存(可选)
    。这一步是为了优化数据访问速度。当有新数据添加后,除了复制元数据外,还希望能够就近访问这些数据。在没有对象存储的环境中,可以结合分布式缓存功能,在每个机房内部署一个分布式缓存集群。然后通过缓存预热,将新增的数据复制到每个边缘集群的缓存集群中,从而加速数据访问。

读数据流程示例

  • 步骤 1:访问镜像的元数据服务
    。如上图绿色编号所示,当 GPU 集群需要获取模型数据时,首先会访问镜像的元数据服务;
  • 步骤 2:读取元数据并获取数据
    。在读取到元数据后,客户端会首先尝试通过机房内的缓存集群获取所需数据。如果之前进行了缓存预热,那么大多数情况下可以直接在机房内的缓存集群中命中所需的模型数据;
  • 步骤 3:回源数据
    。如果由于某种原因未能在缓存集群中找到数据,也无需担心,因为所有缓存集群的节点都会自动回源至中心的对象存储桶中获取最终的原始数据。

因此,整个数据读取流程是畅通无阻的。即使部分数据未被预热或新数据尚未预热成功,也可以通过自动回源的方式,从中心的 JuiceFS 存储桶中拉取数据。

挑战 3:低成本高效读取海量存量数据

除了多云、混合云架构下数据分发的挑战,还有一个常见的需求,在与多家大模型公司的交流中,我们了解到许多公司希望将其积累的大量原始数据(如数 PB 级别)直接迁移到 JuiceFS 中。这种需求增加了大规模数据管理的复杂性,并可能需要进行数据双写等调整,这些都可能影响业务流程的正常运作。

JuiceFS 企业版的「导入对象存储元数据」功能使得企业可以更高效地完成数据导入,同时减少对业务的侵入性。用户无需进行数据拷贝,只需持续导入元数据即可。同时,导入的数据可以通过 JuiceFS 的分布式缓存进行加速,从而提升数据访问速度。下图是该功能的工作流程示意图:

第一步,导入元数据
。通过 JuiceFS 的命令行工具,用户可以选择性地导入原始数据桶中的部分数据,而不必导入整个存储桶。这一过程主要通过前缀匹配实现,此步骤仅涉及元数据的导入,不拷贝对象存储中的数据,因此导入流程会很快完成。

元数据导入不是一次性的操作,随着原始数据的增加或修改,用户可以再次执行增量导入,无需担心重复导入造成额外开销。每次增量导入时,系统只会导入新增或修改的部分数据的元数据,不会重复导入已处理的文件,从而避免额外负担。

第二步,读取元数据
。当元数据导入到 JuiceFS 后,应用(例如推理任务)便能通过 JuiceFS 客户端访问这些导入的数据。因此,应用可以立即开始执行,无需等待原始数据桶中的数据拷贝到 JuiceFS 中。

第三步,读取数据
。在推理等场景中,通常会配置分布式缓存以优化数据读取。由于在第一步中仅导入了元数据而未导入实际数据,初次通过分布式缓存读取时将无法直接获取数据。

第四步,回源原始桶并缓存数据
。这一步需要通过分布式缓存系统回源到原始数据桶中,从中检索并读取数据。读取完成后,数据会自动缓存到 JuiceFS 的分布式缓存中,这样在后续访问相同数据时,就无需重新回到原始数据桶中进行数据读取,从而提高数据访问效率。

经过这几个步骤,推理任务便能够快速访问存量数据,并获得高性能分布式缓存的加速效果。

挑战 4:在异构环境中,如何充分利用硬件资源以优化存储和计算性能?

异构环境涉及到一个系统内部集成多种不同类型或配置的硬件设备,只有充分利用异构的硬件资源才能为企业带来最大价值。在下面这个示例中,我们有三台机器,每台机器配备的 SSD 数量和容量如下表所示,根据每台机器的总存储容量,这三台机器的缓存容量比例为 1:2:3。

编号 SSD 数量 单块 SSD 容量(TB) 总容量(TB)
机器 1 2 4 8
机器 2 2 8 16
机器 3 3 8 24

默认情况下,JuiceFS 的分布式缓存假设所有机器的硬件配置是同构的,因此所有缓存节点的权重相同。在这种配置下,整个系统的性能将被最小容量机器的容量上限所限制,在这个示例中是 8TB,其它机器缓存盘无法被充分利用,第三台机器中甚至有 ⅔ 可能未被利用。

为了避免这种情况,我们引入了「缓存节点权重」的概念,允许用户根据实际环境动态或静态地调整每个 GPU 节点的权重
。例如,第一台 GPU 服务器的缓存权重可以设置为默认值 100,第二台为 200,第三台为 300,这些权重与 SSD 容量的比例(1:2:3)相对应。通过这种差异化权重设置,可以更有效地利用各缓存机器的存储资源,优化整体系统的性能。这种方法为处理不同硬件配置的机器提供了一个典型的解决方案。

除了上述这个场景外,缓存节点权重还可以应用于其它场景。例如,GPU 机器容易出现故障,用户可能每周需要对一两台机器进行下线和更换硬件等常规运维操作。因机器直接停机将导致该机器上的缓存数据丢失或暂时无法访问,这可能影响整个缓存集群的命中率。在这个场景中,也可以使用「缓存节点权重」功能,来尽可能减少机器故障或维护过程中对缓存集群利用率的影响。

未来展望

最后,让我们探讨一下未来我们在推理场景以及其它潜在应用场景中将要进行哪些改进。

首先,引入分布式缓存的多副本特性。目前,分布式缓存系统中的数据通常是单副本形式,意味着如果某台机器(如 GPU 服务器)意外宕机,该机器上的缓存数据将因缺乏备份而丢失,从而直接影响缓存命中率。由于这种情况是突发的,我们无法通过人工干预来逐步迁移数据至其它节点。

在这种背景下,单副本缓存将不可避免地影响整个缓存集群的效率。因此,我们正在考虑将其从单副本升级为多副本。这种升级的好处显而易见:尽管使用了更多的存储空间,但是可以显著提高机器频繁故障场景的缓存命中率和缓存的可用性。

第二点,我们正在探索用户态客户端的实现。当前,基于 FUSE 挂载方式的文件系统虽然能有效地实现文件系统功能,但由于其依赖 Linux 系统内核,涉及用户态与内核态之间的多次切换和数据拷贝,因此带来了一定的性能开销。尤其在云上的无服务器(serverless)和 Kubernetes 环境中,FUSE 挂载可能无权限使用,这限制了 JuiceFS 的应用场景。

因此,我们正在考虑开发一个纯用户态的客户端,这将是一个不依赖内核态的组件,可以显著降低使用门槛,并在不支持 FUSE 的环境中提供服务。此外,由于避免了内核态与用户态的频繁切换和内存拷贝,这种客户端在性能上也可能有显著提升,特别是在需要高吞吐量的 GPU 密集型环境中。

然而,这种客户端的一个潜在缺点是它可能不如 POSIX 接口透明,因为它可能需要用户通过引入特定的库(如 JuiceFS 库)来实现功能,这种方式可能会对应用程序产生一定的侵入性。

第三,提升可观测性。鉴于 JuiceFS 架构中包含多个复杂环节,如从 GPU 机器到缓存集群,再通过专线回到中心的对象存储,以及缓存预热等,我们计划引入更便捷的工具和方法来增强整体架构的可观测性。这将有助于 JuiceFS 的用户更快更方便地定位及分析问题。未来我们将进一步优化包括分布式缓存在内的各个组件的可观测性,帮助用户在出现问题时进行快速的问题排查和解决。

希望这篇内容能够对你有一些帮助,如果有其他疑问欢迎加入
JuiceFS 社区
与大家共同交流。

AI
是什么?

Artificial Intelligence,即人工智能,1956年于Dartmouth学会上提出,一种旨在以类似人类反应的方式对刺激做出反应并从中学习的技术,其理解和判断水平通常只能在人类的专业技能中找到。AI因具备自主学习和认知能力,可进行自我调整和改进,从而应对更加复杂的任务。

AGI

Artificial General Intelligence (AGI),通用人工智能,是具备与人类同等智能、或超越人类的人工智能,能表现正常人类所具有的所有智能行为。又名强人工智能。

PGC

专业生成内容,由专业的创作者、机构或团队制作的内容;CCTV、央视网、人民日报是 PGC 平台;

UGC

用户生成内容,指的是由普通用户创建和分享的内容;抖音、bilibii、小红书都是这样的内容平台;

AIGC

全称"AI generated content”,意为人工智能生成内容,是一种内容生产形式。例如AI文字续写,文字转像的AI图、AI主持人等,都属于AIGC的应用。

ANI

Artificial Narrow Intelligence(ANI),狭义的人工智能,即专注一件事的 AI,如下围棋的AlphaGo。又名弱人工智能。

ASI

Artificial Super Intelligence(ASI),尽管存在争议,但ASI通常被定义为超越人类思维能力的人工智能。

GPTS

GPT插件

GPT store

GPTs 汇集的地方叫做 GPT store;

LLM

Large Language Model,中文意思是 "大型语言模型"。这种大模型通常是指基于深度学习技术的神经网络模型,用于自然语言处理(NLP)任务。

Backpropagation

“误差反向传播”的简称,是一种与最优化方法(如梯度下降法)结合使用的,用来训练人工神经网络的常见方法。该方法计算对网络中所有权重计算损失函数的梯度。这个梯度会反馈给最优化方法,用来更新权值以最小化损失函数。

Agents

Agent(智能体)=一个设置了一些目标或任务,可以迭代运行的大型语言模型。这与大型语言模型(LLM)在像ChatGPT这样的工具中“通常”的使用方式不同。在ChatGPT中,你提出一个问题并获得一个答案作为回应。而Agent拥有复杂的工作流程,模型本质上可以自我对话,而无需人类驱动每一部分的交互。

CNN

Convolutional Neural Network(CNN),一种深度学习模型,通过应用一系列过滤器来处理具有网格状拓扑(例如图像)的数据。此类模型通常用于图像识别任务,

RNN

循环神经网络,一种用于处理序列数据的神经网络, 用于预测文字,但越靠近填空位的词权重越大

Transformers

一个由谷歌提出来的机器学习框架,引入了注意力概念,与RNN不同的是,在预测文字时候,会根据上下文(关键词)进行预测

NLP

Natural Language Processing
自然语言处理,使计算机能够理解、解释和生成人类语言的技术。

DALL-E是什么
OpenAI在2021年发布的一个模型,它能够根据文本描述生成相应的图像

CHATGPT

ChatGPT是OpenAI开发的人工智能聊天机器人程序,于2022年11月推出。该程序使用基于GPT-3.5、GPT-4架构的大型语言模型并以强化学习训练,可以理解和产生人类语言。它像一个机器人聊天伙伴,你可以和它交谈来获取信息或解答问题。

ChatBot

一种计算机程序,旨在通过文本或语音交互模拟人类对话。聊天机器人通常利用自然语言处理技术来理解用户输入并提供相关响应。

COT

思维链提示(CoT,Chain-of-thought)通过提示 LLM 生成-系列中间步骤来提高 LLM 的推理能力,这些中间步骤会导致多步骤问题的最终答案。该技术由谷歌研究人员于 2022 年首次提出。

Deep Learing(DL)

深度学习是机器学习(ML)的分支,是一种以人工神经网络为架构,对资料进行表征学习的算法。深度学习中的形容词“深度”是指在网络中使用多层。

Embedding

在计算机科学中,"embedding"是一种将对象(如词语、用户或商品)映射到数值向量的技术。这些向量捕捉了对象之间的相似性和关系,就像你在"猜词"游戏中使用相关词描述一个词一样。
Embedding 的核心属性是把高维的,可能是非结构化的数据,转化为低维的,结构化的向量。这样做的目的是让机器可以理解和处理这些数据,从而进行有效的学习和预测。
以推荐系统为例,如果我们想要推荐相似的商品给用户,我们可以用 embedding 技术把每个商品转化为一个向量。在这个向量空间中,相似的商品会有相似的向量。当一个用户喜欢某个商品时,我们就可以找到向量空间中最接近这个商品的其他商品,推荐给用户。这就是 embedding 在现实生活中的一个应用,

Emergence

涌现(英语:emergence)或称创发、突现、呈展、演生,是一种现象,为许多小实体相互作用后产生了大实体,而这个大实体展现了组成它的小实体所不具有的特性。涌现在整合层次和复杂系统理论中起着核心作用.例如,生物学中的生命现象是化学的一个涌现。

Few-Shot

小样本学习也叫做少样本学习(low-shot learning),其目标是从少量样本中学习到解决问题的方法,与小样本学习相关的概念还有零样本学习(zero-shot learning)等,零样本学习是指在没有训练数据的情况下,利用类别的属性等信息训练模型,从而识别新类别。

Fine-Tuning

微调是迁移学习的一种常用技术。目标模型复制了源模型上除掉了输出层外的所有模型设计及其参数,并基于目标数据集微调这些参数。微调在自然语言处理(NLP)中很常见,尤其是在语言建模领域。像OpenAI的GPT这样的大型语言模型可以在下游 NLP 任务上进行微调,以产生比预训练模型通常可以达到的更好的结果,
简单的说就是,在通用大模型基础上,再针对具体数据集进行
训练

  1. 全量微调FFT(Full Fine Tuning):对全量的模型参数,进行全量的训练
  2. PEFT(Parameter-Efficient Fine Tuning):只对部分模型参数进行训练

RAG(Retrieval-Augmented Generation)

基于检索增强的生成

Forward Propagation

在神经网络中,前向传播是输入数据被馈送到网络并通过每一层(从输入层到隐藏层,最后到输出层)以产生输出的过程。网络对输入应用权重和偏差,并使用激活函数生成最终输出。

Generative AI / Gen AI

AI 的一个分支,专注于创建模型,这些模型可以根据现有数据的模式和示例生成新的原创内容,例如图像、音乐或文本。

Instruction Tuning

指令调优,机器学习中的一种技术,其中模型根据数据集中给出的特定指令进行微调。

多模态

指多种输入输出渠道,具体有文字,语音,图片,视频等

Temperature

改变温度参数会改变模型的输出(仅限于API)。温度参数可以设置为0到 2。较高的值(例如 0.7)将使输出(概率低)更随机,并产生更多发散的响应,而较小的值(例如 0.2)将使输出(概率高)更加集中和具体。

Top-p

Top-p(前p%筛选):Top-p参数用于控制生成文本时,只考虑累积概率高于给定阈值的词语。0.1表示只考虑概率累积高于10%的词语。这有助于生成更加有连贯性的文本,因为只选择了高概率的选项。如果您希望生成文本更加开放,可以适度增加Top-p值,例如,将其设置为0.5,以考虑更多的选择。

Top-k

Top-k参数用于控制生成文本时只考虑累积概率最高的k个词语。在这里,设置为5,表示只考虑概率最高的5个词语。这有助于生成文本时限制选择范围,以避免选择过多的不太可能的词语。如果您希望生成的文本更加多样,可以增加Top-k的值,例如,将其设置为10或更高。