本仓库对 llama2.c 中的推理文件进行逐行详细解析。内容非常详尽,适合初学者阅读。
你需要对 Transformer 架构有一定了解。如果你是完全的新手,请先参考这篇excellent 博客。
(vocab,768) x (768,1)
的矩阵乘法。基本上是给出每个下一个词元的概率。(1次矩阵乘法)代码分为3个部分:结构体、函数和 main()
中的读取逻辑。我们将首先看结构体,然后进入 main(),最后介绍重要的函数。
注意:代码取自提交 4e23ad83。原始仓库可能因为新的提交而有所不同。 但 99% 的逻辑应该保持不变 :)
我们定义了3个结构体,用于存储模型配置、模型权重和前向传播过程中的中间值(运行状态)
n_layers
, vocab_size
:层数(例如 llama-2 有 32 层/BERT-base 有 12 层)和词汇表中的词元数量(英语通常是 30k)dim
和 hidden_dim
:定义 Q、K、V 和 O 的形状 (dim,dim)
,以及 W1、W2 (dim, hidden_dim)
和 W3 (hidden_dim, dim)
n_heads
:查询(Q)的头数。如果 n_heads=12
,则矩阵 Q=(768,768)
的行为/视为 (768, 768/12,768)
n_kv_heads
:K 和 V 的头数。为什么这些与上面不同?:阅读多查询论文seq_len
:我们将生成的词元数量typedef struct { int dim; // transformer 维度 int hidden_dim; // 用于前馈网络层 int n_layers; // 层数 int n_heads; // 查询头数 int n_kv_heads; // 键/值头数(可以 < 查询头数,因为多查询) int vocab_size; // 词汇表大小,通常为 256(字节级) int seq_len; // 最大序列长度 } Config;
ffn=nn.Linear(...)
的对应部分。
float*
?因为所有矩阵都只是一维展平数组。见下图rms_
是用于 归一化的权重,freq_cis_
用于 RoPE 嵌入。我们稍后会详细介绍 RoPE
。wcls
是最终的分类器。大小为 (vocab, dim)
的矩阵,将最终嵌入从向量映射到词汇表中每个词元的概率。<img src="https://yellow-cdn.veclightyear.com/0a4dffa0/bcd07c1b-b1af-4b07-9909-c8133326f05e.png" width="700" height="200">typedef struct { // 词元嵌入表 float* token_embedding_table; // (vocab_size, dim) // rmsnorms 的权重 float* rms_att_weight; // (layer, dim) rmsnorm 权重 float* rms_ffn_weight; // (layer, dim) // 矩阵乘法的权重 float* wq; // (layer, dim, dim) float* wk; // (layer, dim, dim) float* wv; // (layer, dim, dim) float* wo; // (layer, dim, dim) // 前馈网络的权重 float* w1; // (layer, hidden_dim, dim) float* w2; // (layer, dim, hidden_dim) float* w3; // (layer, hidden_dim, dim) // 最终的 rmsnorm float* rms_final_weight; // (dim,) // RoPE 相对位置嵌入的 freq_cis float* freq_cis_real; // (seq_len, dim/2) float* freq_cis_imag; // (seq_len, dim/2) // (可选)最后一层的 logits 分类器权重 float* wcls; } TransformerWeights;
key_cache
和 value_cache
存储先前词元的 key 和 value 输出。例如,在生成第 5 个词元时,这将存储前 4 个词元的 key
和 value
。typedef struct { // 当前激活波 float *x; // 当前时间戳的激活 (维度,) float *xb; // 相同,但在残差分支内 (维度,) float *xb2; // 仅为方便而设的额外缓冲区 (维度,) float *hb; // 前馈网络中隐藏维度的缓冲区 (隐藏维度,) float *hb2; // 前馈网络中隐藏维度的缓冲区 (隐藏维度,) float *q; // 查询 (维度,) float *k; // 键 (维度,) float *v; // 值 (维度,) float *att; // 分数/注意力值的缓冲区 (注意力头数, 序列长度) float *logits; // 输出logits // kv缓存 float* key_cache; // (层数, 序列长度, 维度) float* value_cache; // (层数, 序列长度, 维度) } RunState;
我们将在遇到函数时进行查看。现在让我们看看 main()
内的逻辑
获取命令行参数。没什么特别的。目前你可以通过以下方式调用 run.c
:
./run llama2_7b.bin
./run llama2_7b.bin 0.1
-> 带温度参数./run llama2_7b.bin 0.1 100
-> 带温度参数和步数(生成的输出token数)最后声明 config
和 weights
int main(int argc, char *argv[]) { // 简易C语言参数解析 char *checkpoint = NULL; // 例如 out/model.bin float temperature = 0.9f; // 例如 1.0 或 0.0 int steps = 256; // 运行的最大步数,0:使用seq_len // 'checkpoint' 是必要参数 if (argc < 2) { printf("用法: %s <checkpoint文件> [温度] [步数]\n", argv[0]); return 1; } if (argc >= 2) { checkpoint = argv[1]; } if (argc >= 3) { // 可选温度。0.0 = (确定性)argmax采样。1.0 = 基准 temperature = atof(argv[2]); } if (argc >= 4) { steps = atoi(argv[3]); } // 用时间作为随机数种子。如果你想要确定性行为,使用温度0.0 srand((unsigned int)time(NULL)); // 读取model.bin文件 Config config; TransformerWeights weights;
checkpoint
文件。
config.json
和 model.bin
是分开的(我们像字典一样加载权重)。但这里 train.py
以特定格式将所有内容保存在一个 .bin
文件中。这种特定格式使我们能够轻松地先读取配置,然后逐一读取每个权重。详细信息
shared_weights
:输入嵌入矩阵和输出分类器矩阵是否应该相同?weights
。通过 file_size = ftell(file);
获取文件大小。与普通PyTorch推理不同,我们不将所有权重加载到RAM中。相反,我们在需要时懒加载,调用 mmap(..)
来分配RAM内存。更多详情请阅读checkpoint_init_weights
(下面是函数片段)。这里我们将权重指针映射到 mmap
返回的正确地址。由于我们已经读取了配置,我们在 float* weights_ptr = data + sizeof(Config)/sizeof(float);
这行中为其偏移。void checkpoint_init_weights(TransformerWeights *w, Config* p, float* f, int shared_weights){ float* ptr = f; w->token_embedding_table = ptr; ptr += p->vocab_size * p->dim; w->rms_att_weight = ptr; ....... }
上述部分讨论的原始代码
int fd = 0; float* data = NULL; long file_size; { FILE *file = fopen(checkpoint, "rb"); if (!file) { printf("无法打开检查点文件 %s!\n", checkpoint); return 1; } // 读取配置头 if(fread(&config, sizeof(Config), 1, file) != 1) { return 1; } // 负的词汇表大小是表示非共享权重的一种取巧方式。有点奇怪。 int shared_weights = config.vocab_size > 0 ? 1 : 0; config.vocab_size = abs(config.vocab_size); // 确定文件大小 fseek(file, 0, SEEK_END); // 将文件指针移到文件末尾 file_size = ftell(file); // 获取文件大小,以字节为单位 fclose(file); // 将Transformer权重内存映射到data指针 fd = open(checkpoint, O_RDONLY); // 以只读模式打开 if (fd == -1) { printf("打开失败!\n"); return 1; } data = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0); if (data == MAP_FAILED) { printf("mmap失败!\n"); return 1; } float* weights_ptr = data + sizeof(Config)/sizeof(float); checkpoint_init_weights(&weights, &config, weights_ptr, shared_weights); }
vocab
是 char**
类型,因为每个标记都是一个字符串,而 vocab
是标记列表。vocab_size
进行循环,读取每个标记// 目前我们无法运行超过 config.seq_len 步骤 if (steps <= 0 || steps > config.seq_len) { steps = config.seq_len; } // 读取 tokenizer.bin 文件 char** vocab = (char**)malloc(config.vocab_size * sizeof(char*)); { FILE *file = fopen("tokenizer.bin", "rb"); if (!file) { printf("无法打开分词器文件 tokenizer.bin!运行 " "python tokenizer.py 将 tokenizer.model 转换为 tokenizer.bin\n"); return 1; } int len; for (int i = 0; i < config.vocab_size; i++) { if(fread(&len, sizeof(int), 1, file) != 1) { return 1; } vocab[i] = (char *)malloc(len + 1); if(fread(vocab[i], len, 1, file) != 1) { return 1; } vocab[i][len] = '\0'; // 添加字符串结束符 } fclose(file); }
token
是 BOS 标记("语句开始"),其词汇索引为 1
。<img src="https://yellow-cdn.veclightyear.com/0a4dffa0/e0fbcfa8-0d24-423b-9999-a39cd36e0e09.jpeg" width="600" height="500">RunState state; malloc_run_state(&state, &config); // 当前位置 long start = time_in_ms(); int next; int token = 1; // 1 = Llama-2 sentencepiece 中的 BOS 标记 int pos = 0; printf("<s>\n"); // 显式打印初始 BOS 标记(=1),在风格上对称
transformer(token, pos, &config, &state, &weights);
将每个标记作为序列中下一个标记的分类器得分存储在 state.logits
中。(transformer
函数的内容将在下一节介绍)
接下来我们进行采样。为什么需要采样以及如何进行?
llama
给出每个标记作为下一个词的得分。例如,假设我们的标记是 ["苹果", "足球", "父亲", "兄弟"]
,llama 给它们的得分是 [0.3, 0.1, 0.9, 0.7]
。现在要选择下一个标记,我们可以选择最高分(得分为 0.9 的 "父亲"
),或者我们以与得分成比例的概率对标记进行采样,这样我们可以在预测中获得更多的多样性(在当今世界非常重要😁)。让我们讨论更多细节:如果 temperature=0
,那就是最大采样。对于 temperate>0
,我们使用 softmax 将 state.logits
转换为概率,并存回 state.logits
。sample(..)
函数从 state.logits
概率分布中采样返回一个标记。在这里阅读更多内容
生成的标记 next
成为下一个输入标记,在 token=next
这一行。
while (pos < steps) { // 前向传递 transformer 以获取下一个标记的 logits transformer(token, pos, &config, &state, &weights); // 采样下一个标记 if(temperature == 0.0f) { // 贪婪最大值采样 next = argmax(state.logits, config.vocab_size); } else { // 对 logits 应用温度 for (int q=0; q<config.vocab_size; q++) { state.logits[q] /= temperature; } // 对 logits 应用 softmax 以获取下一个标记的概率 softmax(state.logits, config.vocab_size); // 现在我们要从这个分布中采样以获取下一个标记 next = sample(state.logits, config.vocab_size); } printf("%s", vocab[next]); fflush(stdout); // 前进 token = next; pos++; }
从 main()
调用的 transformer(token, pos, &config, &state, &weights);
的详细信息
以下部分大量使用 2d/3d 数组索引。我们在这里简要介绍,以使生活更轻松
float* mat
的大小为 (dim1, dim2, dim3)
,那么访问 mat[l][i][j]
的指针是 dim2*dim3*l + dim3*i + j;
- 这是 公式-1
,我们稍后会经常引用。如果你感到困惑,请阅读链接如何从头的角度看待矩阵?
float* wk
是一个形状为 (layer, dim, dim)
的矩阵,从头的角度来看是 (layer, dim, n_heads, head_dim)
memcpy
将token
的嵌入复制到s->xb
中,没有什么特别之处。为什么不直接使用float* content_row
?因为s->xb
将会发生变化,而使用content_row
会改变模型权重。void transformer(int token, int pos, Config* p, RunState* s, TransformerWeights* w) { // 一些便利变量 float *x = s->x; int dim = p->dim; int hidden_dim = p->hidden_dim; int head_size = dim / p->n_heads; float* content_row = &(w->token_embedding_table[token * dim]); // 将token嵌入复制到x中 memcpy(x, content_row, dim*sizeof(*x));
RoPE:旋转位置嵌入
[0.8, 0.5, -0.1, 0.3]
,我们将它们分组成对:[[0.8,-0.1], [0.5, 0.3]
,然后旋转某个角度$\theta$。这个$\theta$从一开始就是固定的(不可学习)。在论文中,$\theta_{i}$的值是$10000^{2(i-1)/d}$。RoPE公式(对于分组成对的2个特征)如下。$m$是对的索引。$\theta$是我们从.bin
文件加载的固定参数。
$$ \left[ {\begin{array}{ccccc} x_{m}^{i} & x_{m}^{j} \ \end{array} } \right] * \left[ {\begin{array}{ccccc} cos(m\theta_{m}) & -sin(m\theta_{m}) \ sin(m\theta_{m}) & cos(m\theta_{m}) \ \end{array} } \right] $$
我们的示例对[[0.8,-0.1], [0.5, 0.3]
将被如下转换。 请记住,对于第一对[0.8, 0.1]
,$m=0$(因此$sin(0)=0$)。对于第二对,$m=1$。
$$ \left[ {\begin{array}{ccccc} 0.8 & -0.1 \ \end{array} } \right] * \left[ {\begin{array}{ccccc} 1 * 1 & -0.0 * 1 \ 0.0 * 1 & 1.0 * 1 \ \end{array} } \right] = \left[ {\begin{array}{ccccc} 0.8 & -0.1 \ \end{array} } \right] $$
$$ \left[ {\begin{array}{ccccc} 0.5 & 0.3 \ \end{array} } \right] * \left[ {\begin{array}{ccccc} 0.86 * 1 & -0.5 * 1 \ 0.5 * 1 & 0.86 * 1 \ \end{array} } \right] = \left[ {\begin{array}{ccccc} 0.58 & 0.08 \ \end{array} } \right] $$
将两者结合,输出为[[0.8, 0.1], [0.58, 0.08]]
,现在解对它们将得到[0.8, 0.58, 0.1, 0.08]
。
所以RoPE
将[0.8, 0.5, -0.1, 0.3]
转换为[0.8, 0.58, -0.1, 0.08]
。请记住,如果特征的dim=768
,那么有一半的384个$\theta$。
回到代码
pos
是我们的$m$)。freq_cis_real_row
是$cos(m\theta)$,freq_cis_imag_row
是$sin(m\theta)$。// 提取freq_cis_real和freq_cis_imag的"pos"行 float* freq_cis_real_row = w->freq_cis_real + pos * head_size / 2; float* freq_cis_imag_row = w->freq_cis_imag + pos * head_size / 2;
rmsnorm
。rmsnorm
函数计算如下:out\; = \; (x*g*n)/\sum_{i} \sqrt{x_{i}^{2}}
其中$x$是输入,$g$是可学习参数(下面的w->rms_attn_weight
),$n$是dim
。
matmul
对2D矩阵和1D矩阵进行矩阵乘法。即(A, B) x (A,)
。其实现非常简单(我们将在最后讨论)。我们将Q、K、V与s->xb
(rmsnorm
的输出)相乘,并将结果存储在s->q
、s->k
等中。
for(int l = 0; l < p->n_layers; l++) { // 注意力层的rmsnorm rmsnorm(s->xb, x, w->rms_att_weight + l*dim, dim); // 对当前位置进行qkv矩阵乘法 matmul(s->q, s->xb, w->wq + l*dim*dim, dim, dim); matmul(s->k, s->xb, w->wk + l*dim*dim, dim, dim); matmul(s->v, s->xb, w->wv + l*dim*dim, dim, dim);
s->q
和s->k
。我们对每个头分别进行处理,因此取h*head_size
的偏移量。// 对每个注意力头的q和k向量应用RoPE旋转 for (int h = 0; h < p->n_heads; h++) { // 获取此注意力头的q和k向量 float* q = s->q + h * head_size; float* k = s->k + h * head_size; // 使用freq_cis_real和freq_cis_imag旋转q和k for (int i = 0; i < head_size; i+=2) { float q0 = q[i]; float q1 = q[i+1]; float k0 = k[i]; float k1 = k[i+1]; float fcr = freq_cis_real_row[i/2]; float fci = freq_cis_imag_row[i/2]; q[i] = q0 * fcr - q1 * fci; q[i+1] = q0 * fci + q1 * fcr; k[i] = k0 * fcr - k1 * fci; k[i+1] = k0 * fci + k1 * fcr; } }
一旦我们获得当前标记的q、k、v
,我们需要计算自注意力。这里我们将查询乘以键。k
和v
仅用于当前标记。我们将所有过去标记的k、v
存储在key_cache_row
和value_cache_row
中。
seq_length
)的键和查询,其维度为(layer, seq_length, dim)
。seq_length
通常被称为context
。考虑上述示例中的以下代码。假设seq_length=32
(这意味着我们最多生成32个标记)。pos=2
,因为"fox"是第3个标记(Python从0开始索引,所以是第2个)。
s->key_cache
中填充了layer*(pos-1)*dim
个值。在进行自注意力之前,我们还需要将当前标记"fox"的键和值填入s->key_cache
。这就是memcpy(key_cache_row, s->k, dim*sizeof(*key_cache_row));
所做的事。// 将此时间步(pos)的键值保存到我们的kv缓存中 int loff = l * p->seq_len * dim; // 为方便起见,计算kv缓存层偏移量 float* key_cache_row = s->key_cache + loff + pos * dim; float* value_cache_row = s->value_cache + loff + pos * dim; memcpy(key_cache_row, s->k, dim*sizeof(*key_cache_row)); memcpy(value_cache_row, s->v, dim*sizeof(*value_cache_row));
公式
\begin{align} out = (QK^{T})\;V/\sqrt{d} \\ 其中\;\;\; Q=(1,dim) \;\; K=(dim,N) \;\; V=(dim,N) \end{align}
上述公式中的$N$是pos
(生成文本的当前长度)
如果你记住 s->q
、s->k
在头部维度上的形状为 (dim, n_heads, head_dim)
,而 key_cache
的形状为 (seq_length, n_heads, head_dim)
,这部分代码就变得很简单了。让我们逐步分析代码:
int h
是当前的头计数。让我们逐行看:
q = s->q + h*head_size
:获取第 h 个头的起始指针。记住公式1。矩阵大小为 (dim, n_heads, head_dim)
,我们需要 s->q[0][h][0]
,即 0*n_heads*head_dim + h*head_dim + 0
,也就是 h*head_size
。att = s->att + h * p->seq_len
:我们将注意力存储在运行状态变量 s->attn
中。pos
当前是2)
s->key_cache + l*seq_length*dim + t*n_heads*head_dim + h*head_dim
。由于之前定义的 loff
已经是 l*seq_length*dim
,最终偏移量是 loff + t*n_heads*head_dim + h*head_size
。因为 n_heads*head_dim=dim
,所以我们得到偏移量 loff + t*dim + h*head_size
。现在我们有了 q
(head_size,)
、k
(head_size,)
和 att
(seq_length,)
。 我们可以计算第 h 个头在位置 t 的自注意力分数。我们对所有头和到目前为止的所有位置进行求和。
attn
形状为 (seq_length, )
。接下来我们将其与形状为 (seq_length, dim)
的 v
相乘。记住,下面的循环是在上一节开始的 for (h = 0; h < p->n_heads; h++)
内部。要完成注意力模块,我们需要与 O 相乘,这在第一行完成。下一行 accum
添加来自跳跃连接(红色箭头)的输入和注意力的输出。然后进行归一化。
接下来我们计算 FFN 输出,公式为:
out = W_3 * σ(W_1X * W_2X)
其中 σ 是 silu 激活函数。 这部分是不言自明的
// 现在对于PyTorch中的FFN,我们有:self.w2(F.silu(self.w1(x)) * self.w3(x)) // 首先计算self.w1(x)和self.w3(x) matmul(s->hb, s->xb, w->w1 + l*dim*hidden_dim, dim, hidden_dim); matmul(s->hb2, s->xb, w->w3 + l*dim*hidden_dim, dim, hidden_dim); // F.silu; silu(x)=x*σ(x),其中σ(x)是logistic sigmoid函数 for (int i = 0; i < hidden_dim; i++) { s->hb[i] = s->hb[i] * (1.0f / (1.0f + expf(-s->hb[i]))); } // 与w3(x)进行元素级乘法 for (int i = 0; i < hidden_dim; i++) { s->hb[i] = s->hb[i] * s->hb2[i]; } // 最后的矩阵乘法得到ffn的输出 //memcpy(tmp_w_hid, w->w2 + l*dim*hidden_dim, hidden_dim*dim*sizeof(float)); matmul(s->xb, s->hb, w->w2 + l*dim*hidden_dim, hidden_dim, dim);
accum(x, s->xb, dim);
在对所有层运行上述模块后,我们得到一个形状为(dim,)
的嵌入。我们需要将其转换为形状为(vocab,)
的向量, 其中每个条目告诉我们该词作为下一个标记的得分是多少。
w->wcls
)相乘之前,我们对嵌入进行归一化。得分保存在s->logits
中// 最终rmsnorm rmsnorm(x, x, w->rms_final_weight, dim); // 分类器得到logits matmul(s->logits, x, w->wcls, p->dim, p->vocab_size);
一旦我们得到s->logits
,我们就对下一个标记进行采样(重复此操作直到获得seq_length
个标记)。这在"主函数中的前向循环和采样"部分已经介绍过了。恭喜!现在你知道LLM是如何工作的,以及如何用C语言编写它们。如果你现在想知道如何用Python编写它们,请参考modelling_llama.py
这是一张猫的图片 :)
<img src="https://yellow-cdn.veclightyear.com/0a4dffa0/51639862-ff40-4405-80ec-6ea122447be7.jpg" width="700" height="500">一键生成PPT和Word,让学习生活更轻松
讯飞智文是一个利用 AI 技术的项目,能够帮助用户生成 PPT 以及各类文档。无论是商业领域的市场分析报告、年度目标制定,还是学生群体的职业生涯规划、实习避坑指南,亦或是活动策划、旅游攻略等内容,它都能提供支持,帮助用户精准表达,轻松呈现各种信息。
深度推理能力全新升级,全面对标OpenAI o1
科大讯飞的星火大模型,支持语言理解、知识问答和文本创作等多功能,适用于多种文件和业务场景,提升办公和日常生活的效率。讯飞星火是一个提供丰富智能服务的平台,涵盖科技资讯、图像创作、写作辅助、编程解答、科研文献解读等功能,能为不同需求的用户提供便捷高效的帮助,助力用户轻松获取信息、解决问题,满足多样化使用场景。
一种基于大语言模型的高效单流解耦语音令牌文本到语音合成模型
Spark-TTS 是一个基于 PyTorch 的开源文本到语音合成项目,由多个知名机构联合参与。该项目提供了高效的 LLM(大语言模型)驱动的语音合成方案,支持语音克隆和语音创建功能,可通过命令行界面(CLI)和 Web UI 两种方式使用。用户可以根据需求调整语音的性别、音高、速度等参数,生成高质量的语音。该项目适用于多种场景,如有声读物制作、智能语音助手开发等。
字节跳动发布的AI编程神器IDE
Trae是一种自适应的集成开发环境(IDE),通过自动化和多元协作改变开发流程。利用Trae,团队能够更快速、精确地编写和部署代码,从而提高编程效率和项目交付速度 。Trae具备上下文感知和代码自动完成功能,是提升开发效率的理想工具。
AI助力,做PPT更简单!
咔片是一款轻量化在线演示设计工具,借助 AI 技术,实现从内容生成到智能设计的一站式 PPT 制作服务。支持多种文档格式导入生成 PPT,提供海量模板、智能美化、素材替换等功能,适用于销售、教师、学生等各类人群,能高效制作出高品质 PPT,满足不同场景演示需求。
选题、配图、成文,一站式创作,让内容运营更高效
讯飞绘文,一个AI集成平台,支持写作、选题、配图、排版和发布。高效生成适用于各类媒体的定制内容,加速品牌传播,提升内容营销效果。
专业的AI公文写作平台,公文写作神器
AI 材料星,专业的 AI 公文写作辅助平台,为体制内工作人员提供高效的公文写作解决方案。拥有海量公文文库、9 大核心 AI 功能,支持 30 + 文稿类型生成,助力快速完成领导讲话、工作总结、述职报告等材料,提升办公效率,是体制打工人的得力写作神器。
OpenAI Agents SDK,助力开发者便捷使用 OpenAI 相关功能。
openai-agents-python 是 OpenAI 推出的一款强大 Python SDK,它为开发者提供了与 OpenAI 模型交互的高效工具,支持工具调用、结果处理、追踪等功能,涵盖多种应用场景,如研究助手、财务研究等,能显著提升开发效率,让开发者更轻松地利用 OpenAI 的技术优势。
高分辨率纹理 3D 资产生成
Hunyuan3D-2 是腾讯开发的用于 3D 资产生成的强大工具,支持从文本描述、单张图片或多视角图片生成 3D 模型,具备快速形状生成能力,可生成带纹理的高质量 3D 模型,适用于多个领域,为 3D 创作提供了高效解决方案。
一个具备存储、管理和客户端操作等 多种功能的分布式文件系统相关项目。
3FS 是一个功能强大的分布式文件系统项目,涵盖了存储引擎、元数据管理、客户端工具等多个模块。它支持多种文件操作,如创建文件和目录、设置布局等,同时具备高效的事件循环、节点选择和协程池管理等特性。适用于需要大规模数据存储和管理的场景,能够提高系统的性能和可靠性,是分布式存储领域的优质解决方案。
最新AI工具、AI资讯
独家AI资源、AI项目落地
微信扫一扫关注公众号