diff --git a/_posts/MachineLearning/2021-08-18-gpu.md b/_posts/MachineLearning/2021-08-18-gpu.md index af404979..ef5adae7 100644 --- a/_posts/MachineLearning/2021-08-18-gpu.md +++ b/_posts/MachineLearning/2021-08-18-gpu.md @@ -15,6 +15,8 @@ keywords: gpu [CPU 和 GPU - 异构计算的演进与发展](https://draveness.me/heterogeneous-computing/)世界上大多数事物的发展规律是相似的,在最开始往往都会出现相对通用的方案解决绝大多数的问题,随后会出现为某一场景专门设计的解决方案,这些解决方案不能解决通用的问题,但是在某些具体的领域会有极其出色的表现。 +GPU 的架构;内存管理;任务管理;数据类型。 + ## GPU 各种游戏里面的人物的脸,并不是那个相机或者摄像头拍出来的,而是通过多边形建模(Polygon Modeling)创建出来的。而实际这些人物在画面里面的移动、动作,乃至根据光线发生的变化,都是通过计算机根据图形学的各种计算,实时渲染出来的。 @@ -78,11 +80,9 @@ CPU和GPU的主要区别在于它们的设计目标。CPU的设计初衷是执 GPU架构总体如下图所示: -![](/public/upload/machine/gpu_sm.jpg) - -SM内部包含什么? +![](/public/upload/machine/gpu_arch.jpg) -![](/public/upload/kubernetes/gpu_arch.png) +![](/public/upload/machine/gpu_sm.jpg) 流式多处理器(Streaming Multiprocessor、SM)是 GPU 的基本单元,每个 GPU 都由一组 SM 构成,SM 中最重要的结构就是计算核心 Core 1. 线程调度器(Warp Scheduler):线程束(Warp)是最基本的单元,每个线程束中包含 32 个并行的线程,它们**使用不同的数据执行相同的命令**,调度器会负责这些线程的调度; @@ -94,6 +94,13 @@ SM内部包含什么? 与个人电脑上的 GPU 不同,数据中心中的 GPU 往往都会用来执行高性能计算和 AI 模型的训练任务。正是因为社区有了类似的需求,Nvidia 才会在 GPU 中加入张量(标量是0阶张量,向量是一阶张量, 矩阵是二阶张量)核心(Tensor Core)18专门处理相关的任务。张量核心与普通的 CUDA 核心其实有很大的区别,**CUDA 核心在每个时钟周期都可以准确的执行一次整数或者浮点数的运算**,时钟的速度和核心的数量都会影响整体性能。**张量核心通过牺牲一定的精度可以在每个时钟计算执行一次 4 x 4 的矩阵运算**。PS:就像ALU 只需要加法器就行了(乘法指令转换为多个加法指令),但为了提高性能,直接做了一个乘法器和加法器并存。 + +CUDA 编程主打一个多线程 thread,多个 thread 成为一个 thread block,同一个 block 内的 thread 共享Shared Memory/L1 cache/SRAM,而 thread block 就是由这么一个 Streaming Multiprocessor (SM) 来运行的。 +1. 一个 SM 里面有多个 subcore,每个 subcore 有一个 32 thread 的 warp scheduler 和 dispatcher, 在一个 warp 中的所有线程都会同时执行相同的指令,但是输入的数据不同,这种机制也被称为 SIMD(单指令多数据)或 SIMT(单指令多线程)模型。 +2. GPU 的调度单元以 warp 为单位进行调度,而不是单个线程。这意味着整个 warp 会被分配到一个流多处理器(SM)上并一起执行。在 CUDA 中,占用率是一个重要的性能指标,表示每个 SM 上激活的 warps 与 SM 可以支持的最大 warp 数量的比例。更高的占用率通常意味着更好的硬件利用率。 +3. 如果 warp 中的所有线程都采取相同的分支路径(例如,都满足某个条件语句),则它们会继续同步执行。但是,如果线程在分支上有不同的路径(即分歧),则 warp 会执行每个路径,但不是所有线程都会在每个路径上活跃。这可能导致效率下降,因为即使某些线程在特定路径上没有工作,整个 warp 也必须等待该路径完成。为了确保高效执行,开发人员可能需要确保他们的代码减少 warp 分歧。 +4. Global memory 就是我们常说的 显存 (GPU memory),其实是比较慢的。Global memory 和 shared memory 之间是 L2 cache,L2 cache 比 global memory 快。每次 shared memory 要到 global memory 找东西的时候, 会去看看 l2 cache 里面有没有, 有的话就不用去 global memory 了. 有的概率越大, 我们说 memory hit rate 越高, CUDA 编程的一个目的也是要尽可能提高 hit rate. 尤其是能够尽可能多的利用比较快的 SRAM (shared memory).但是因为 SRAM 比较小, 所以基本原则就是: 每次往 SRAM 移动数据的, 都可能多的用这个数据. 避免来来回回的移动数据. 这种 idea 直接促成了最近大火的 FlashAttention. FlashAttention 发现很多操作计算量不大, 但是 latency 很高, 那肯定是不符合上述的 "每次往 SRAM 移动数据的". 怎么解决呢?Attention 基本上是由 matrix multiplication 和 softmax 构成的. 我们已经知道了 matrix multiplication 是可以分块做的, 所以就剩下 softmax 能不能分块做? softmax 其实也是可以很简单的被分块做的. 所以就有了 FlashAttention. + ### 执行模型 diff --git a/_posts/MachineLearning/2022-03-02-embedding.md b/_posts/MachineLearning/2022-03-02-embedding.md index 74658a25..6d32802d 100644 --- a/_posts/MachineLearning/2022-03-02-embedding.md +++ b/_posts/MachineLearning/2022-03-02-embedding.md @@ -35,7 +35,8 @@ Embedding 技术对深度学习推荐系统的重要性 1. Embedding 是处理稀疏特征的利器。因为推荐场景中的类别、ID 型特征非常多,大量使用 One-hot 编码会导致样本特征向量极度稀疏,而深度学习的结构特点又不利于稀疏特征向量的处理,因此几乎所有深度学习推荐模型都会由 Embedding 层负责将稀疏高维特征向量转换成稠密低维特征向量。 2. Embedding 可以融合大量有价值信息,本身就是极其重要的特征向量 。 相比由原始信息直接处理得来的特征向量,Embedding 的表达能力更强,特别是 Graph Embedding 技术被提出后,Embedding 几乎可以引入任何信息进行编码,使其本身就包含大量有价值的信息,所以通过预训练得到的 Embedding 向量本身就是极其重要的特征向量。 -Word2vec 是生成对“词”的向量表达的模型,其中,Word2vec 的训练样本是通过滑动窗口一一截取词组生成的。在训练完成后,模型输入向量矩阵的行向量,就是我们要提取的词向量。 + +在自然语言处理(NLP)中,嵌入(Embedding)是一种将离散变量(如单词、短语、或者文档)转换为连续向量的方法。这种转换的目的是让计算机能更好地理解和处理自然语言数据。embedding矩阵的本质是一个查找表 ,每个单词会定位这个表中的某一行,而这一行就是这个单词学习到的在嵌入空间的语义。Word2vec 是生成对“词”的向量表达的模型,其中,Word2vec 的训练样本是通过滑动窗口一一截取词组生成的。在训练完成后,模型输入向量矩阵的行向量,就是我们要提取的词向量。 ![](/public/upload/compute/embedding_sample.png) diff --git a/_posts/MachineLearning/2023-05-20-llm_try.md b/_posts/MachineLearning/2023-05-20-llm_try.md index 0eae6d33..d566d0a0 100644 --- a/_posts/MachineLearning/2023-05-20-llm_try.md +++ b/_posts/MachineLearning/2023-05-20-llm_try.md @@ -137,6 +137,8 @@ LangChain实时推荐天气 [大模型元年,万能的淘宝有了万能AI](https://mp.weixin.qq.com/s/QBEr2HwHdab53V97bUYTkw) 购物从需要明确知道要买什么,去搜索。变成了只要有需求,都可以询问AI。 +[大模型BI:商业智能背后的3大关键技术](https://mp.weixin.qq.com/s/kSXcmb8UWDukshH_MQkpDw)LLM就像一个刚进公司的实习生,名牌大学毕业,基础知识储备扎实,但还是需要一个老师傅告诉他每一步该怎么做,他才可能完成任务执行。如果我提出这样的问题:“我该怎么做才能提高整体销量?”,那就要看是零售行业还是制造行业,每个行业的分析思路差别巨大。如何让大模型从一名实习生成长为一名资深从业人员,解决更有难度的问题,首先要深入行业内部,自己成为那个资深人员,才可能知道如何将行业知识与大模型深度融合,这里不是简单的堆数据做训练,然后发布XXX-GPT。 + ## 这一轮技术革命的真正“终局”是什么样子 LLM的强大之一在于其能够将众多的NLP下游任务都转换成统一的形式。即LLM可以定义为求解`P(output | input, task)`的过程。如, diff --git a/_posts/MachineLearning/2023-09-04-llm_source.md b/_posts/MachineLearning/2023-09-04-llm_source.md index 3b1d6d06..495e4870 100644 --- a/_posts/MachineLearning/2023-09-04-llm_source.md +++ b/_posts/MachineLearning/2023-09-04-llm_source.md @@ -108,6 +108,112 @@ print(output) ![](/public/upload/machine/transformers_pipelines.png) +### Dataset + +```python +from datasets import load_dataset +raw_datasets = load_dataset("glue", "mrpc") +print(raw_datasets) +# 包含训练集、验证集和测试集。每一个集合都包含几个列(sentence1, sentence2, label, and idx)以及一个代表行数的变量 +#DatasetDict({ +# train: Dataset({ +# features: ['sentence1', 'sentence2', 'label', 'idx'], +# num_rows: 3668 +# }) +# validation: Dataset({ +# features: ['sentence1', 'sentence2', 'label', 'idx'], +# num_rows: 408 +# }) +# test: Dataset({ +# features: ['sentence1', 'sentence2', 'label', 'idx'], +# num_rows: 1725 +# }) +#}) +``` +load_dataset函数返回的是DatasetDict对象,它类似Python的Dict。DatasetDict里的不同key代表了数据集的不同split(默认的split是train),比如glue数据集包含”train”、”validation”和”test”三个split。DatasetDict的value是Dataset对象,它包含了一个split的全部数据。 + +为了预处理数据集,我们需要将文本转换为模型能够理解的数字,使用Dataset.map()方法 +```python +# example 是一个dict,对应数据集的每个元素,并返回一个包含input_ids、attention_mask 和token_type_ids为key的新dict +# 在机器学习任务中,一个example通常定义为模型的输入(也成为特征集合) +def tokenize_function(example): + return tokenizer(example["sentence1"], example["sentence2"], truncation=True) +tokenized_datasets = raw_datasets.map(tokenize_function, batched=True) +print(raw_datasets) +#DatasetDict({ +# train: Dataset({ +# features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'], +# num_rows: 3668 +# }) +# validation: Dataset({ +# features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'], +# num_rows: 408 +# }) +# test: Dataset({ +# features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'], +# num_rows: 1725 +# }) +#}) +``` +为了解决句子长度统一的问题,我们必须定义一个collate函数,该函数会将每个batch句子填充到正确的长度。 + +```python +from datasets import load_dataset +from transformers import AutoTokenizer, DataCollatorWithPadding + +raw_datasets = load_dataset("glue", "mrpc") +tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased") +def tokenize_function(example): + return tokenizer(example["sentence1"], example["sentence2"], truncation=True) +tokenized_datasets = raw_datasets.map(tokenize_function, batched=True) +data_collator = DataCollatorWithPadding(tokenizer=tokenizer) +``` +加载本地数据集 +```python +from datasets import load_dataset +#{ +# "data": [ +# { +# "title": "Terremoto del Sichuan del 2008", +# "paragraphs": [{...}] +# } +# ], +# "version": "1.1" +#} +squad_it_dataset = load_dataset("json", data_files="SQuAD_it-train.json", field="data") +print(squad_it_dataset) # 加载本地文件会创建一个带有train的DatasetDict 对象 +# DatasetDict({ +# train: Dataset({ +# features: ['title', 'paragraphs'], +# num_rows: 442 +# }) +#}) +data_files = {"train": "SQuAD_it-train.json", "test": "SQuAD_it-test.json"} +squad_it_dataset = load_dataset("json", data_files=data_files, field="data") +print(squad_it_dataset) # 包括 train 和 test 的 DatasetDict 对象 +#DatasetDict({ +# train: Dataset({ +# features: ['title', 'paragraphs'], +# num_rows: 442 +# }) +# test: Dataset({ +# features: ['title', 'paragraphs'], +# num_rows: 48 +# }) +#}) +``` + +与 Pandas 类似,transformers Datasets 提供了几个函数来操作 Dataset 和 DatasetDict 对象 +1. rename_column 重命名DatasetDict中的列 +2. filter 过滤一些行 +3. 为数据集中的所有行创建新的数据列 + ```python + def compute_review_length(example): + return {"review_length": len(example["review"].split())} + drug_dataset = drug_dataset.map(compute_review_length) + ``` +4. 用于预训练 GPT-2 的 WebText 语料库包含超过 800 万个文档和 40 GB 的文本,全加载到计算机内存吃不消,`pubmed_dataset_streamed = load_dataset("json", data_files=data_files, split="train", streaming=True)`,streaming=True 返回的对象是一个 IterableDataset + ### 源码分析 ```python @@ -135,7 +241,10 @@ print(output) # attentions=None #) ``` -`output = model(**tokens)` 对于文本分类来说,整段文本输入返回一个标签,也就是SequenceClassifierOutput.logits,那么对于文本生成来说,整段文本输入返回CausalLMOutputWithPast,它的logits自动补全的是 input_ids 的下一个token呢,还是直到eos的多个token呢?生成式模型的训练是并行的,靠的attention mask操作。推理的时候,只能一个字一个字的推理,所以会调用多次。训练的时候只需要调用一次即可,因为attention mask的机制可以保证当前token的loss不包含后面的token。所以推理的时候, output = model(inputs) 或者 output = model.forward(inputs)的时候,inputs 其实也不需要包含 attention_mask,inputs 仅仅是token 就可以,而训练时,inputs 则是一个dict,包含input_ids 和 attention_mask。 + +`output = model(**tokens)` 对于文本分类来说,整段文本输入返回一个标签,也就是SequenceClassifierOutput.logits,那么对于文本生成来说,整段文本输入返回CausalLMOutputWithPast, 对于 input_ids = [v1,v2,v3] 输出为 [v20,v30,v4]。 v20 是根据v1 生成的下一个token(大概率跟真实的v2 不一样),v4 是根据v1,v2,v3 生成的。 +1. 生成式模型的训练是并行的,靠的attention mask操作。训练的时候只需要调用一次即可,因为attention mask的机制可以保证当前token的loss不包含后面的token。inputs 则是一个dict,包含input_ids 和 attention_mask。 +2. 推理的时候,只能一个字一个字的推理,所以会调用多次。 output = model(inputs) 或者 output = model.forward(inputs)的时候,inputs 其实也不需要包含 attention_mask,inputs 仅仅是token 就可以 model(xx) ==> `Module.__call__` ==> Module.forward/model.forward,几乎每一个llm 都会自定义forward 方法,如果向forward 方法传入 labels,还会自动计算loss。 @@ -208,7 +317,7 @@ class GPT2LMHeadModel(GPT2PreTrainedModel): ![](/public/upload/machine/sequence_mask.jpg) 3. 为什么Attention Mask不是0和1构成的矩阵,而是0和负无穷构成的?在 Transformer 模型中通常用于指示模型哪些位置是有效的输入,哪些位置是填充的。它的主要目的是确保模型在计算注意力分数时不会考虑到填充的位置。在大多数实现中,当我们说“mask”时,我们通常是指一个由0和1组成的矩阵,其中1表示“考虑这个位置”而0表示“不考虑这个位置”。但在实际的注意力机制计算中,这种简单的0和1的表示方法并不直接适用。Transformer中的注意力机制涉及到softmax函数,该函数会将输入的原始分数转换为概率分布。为了确保某些位置在softmax之后的概率为0,我们需要在softmax之前为这些位置赋予一个非常小的分数,通常是负无穷。这样,经过softmax转换后,这些位置的概率会接近于0。 -### generate实现 +### 推理/generate实现 [浅谈LLAMA2核心函数generate源码](https://mp.weixin.qq.com/s/vnke00f7kzlA16Pw_FJFjQ) @@ -305,113 +414,29 @@ def generate(self, ``` 推理的时候不使用mask,要串行执行,只有得到前一个单词,重新进入decoder模块,通过模型推理才能得到下一个输出。 -### Dataset +[以LLAMA为例,快速入门LLM的推理过程](https://mp.weixin.qq.com/s/5lbrqbqiHPZIARsVW6l6tA) 建议细读。 -```python -from datasets import load_dataset -raw_datasets = load_dataset("glue", "mrpc") -print(raw_datasets) -# 包含训练集、验证集和测试集。每一个集合都包含几个列(sentence1, sentence2, label, and idx)以及一个代表行数的变量 -#DatasetDict({ -# train: Dataset({ -# features: ['sentence1', 'sentence2', 'label', 'idx'], -# num_rows: 3668 -# }) -# validation: Dataset({ -# features: ['sentence1', 'sentence2', 'label', 'idx'], -# num_rows: 408 -# }) -# test: Dataset({ -# features: ['sentence1', 'sentence2', 'label', 'idx'], -# num_rows: 1725 -# }) -#}) -``` -load_dataset函数返回的是DatasetDict对象,它类似Python的Dict。DatasetDict里的不同key代表了数据集的不同split(默认的split是train),比如glue数据集包含”train”、”validation”和”test”三个split。DatasetDict的value是Dataset对象,它包含了一个split的全部数据。 +[一网打尽文本生成策略(beam search/top-k/top-p/温度系数)](https://zhuanlan.zhihu.com/p/676398366)假设我们输入 “I am a”,GPT会对这个序列进行编码得到embedding,并预测下一个单词的概率。“I am a”作为输入,编码后也会得到三个token对应的embedding。GPT会使用输入序列最后一个token对应的embedding做预测,也就是说,”a”经过编码后的embedding会被映射到整个词表上,得到下一个单词的概率。因为GPT是使用masked attention的,每个token在编码时都只会和他前面的token交互。那么最后一个token自然就包括了当前序列的所有信息,因此用于预测下一个token是最合理的。当得到了整个词表中的单词作为下一个token的概率之后,GPT是选择概率最大(贪心)的那个作为输出吗?显然不是的。因为使用这样的策略,对于相同的输入GPT每次都会给出一样的回答(推理模式下所有参数都固定,不存在随机性)。而理解GPT回答的多样性就需要介绍一些生成策略了。 +1. Beam search,当输入为“I am a”, 设置num_beams=2,beam search的过程可表达为:假设A,B,C对应的概率分别为0.5,0.2,0.3。那么此时选择每个token并组成序列的概率分别为: + ``` + “I am a A”: 0.5 + “I am a B”: 0.3 + “I am a C”: 0.2 + ``` + beam search会选择概率最大的num_beams=2个序列,参与后续的生成。因此“I am a A”和“I am a B”会参与下一个token的预测。假设得到六个可能序列对应的概率。此时再保留num_beams=2个最大概率的序列,比如是”I am a A C”=0.25, ”I am a B C”=0.27,再把这两个序列送到下一个token的预测中。上述步骤会一直重复,直到遇到结束符或指定的长度。最终,会有num_beams个序列被预测出来。此时可以对这几个概率最大的序列做进一步的处理,例如选一个概率最大的作为最终的输出,或者根据概率做个采样作为输出。beam search有什么好处呢?相比于每次都选最大的贪心,beam search显然增大了搜索空间。而且更重要的是,beam search会防止潜在概率很大的序列被过早的抛弃。例如”I am a B C”,在预测到B的时候序列的概率还是不大的,但是到C就变得更大了。当num_beams=1时,beam search等价于贪心。 +2. Top-k sampling,就是每一步只考虑概率最大的k个token,并且把这k个token的概率做重归一化,并随机采样得到预测的token。假设在一步中,ABC对应的概率ligits是[5,3,2],k设置为2。那么会选出字母A,B,并把其对应的概率logits[5,3]进行重新归一化softmax([5,3]) = [0.88,0.12]。随后基于归一化后的概率随机选A或B,拼接到“I am a”后面,并进行下一个token的预测,如此反复。top-k和beam search有什么区别呢?top-k自始至终只有一个序列进行预测,k只用于规定采样的范围,每步只采样一个token作为结果。而beam search会保留num_beams个序列进行预测。 +3. top-p sampling,这种策略会把token的概率按照递减的次序累加,直到累加的概率值超过了阈值p,在这些token中做采样得到预测。假设p=0.7,ABC在第一步预测的概率分布为[0.5,0.3,0.2]。那么A和B的概率值加起来超过了0.7,第一步就会在A,B中采样得到预测。假设第二步概率分布为[0.3,0.3,0.4],那么ABC三个加起来才会超过0.7,此时第二步就会在这三个里面采样,如此反复。可以看出top-p的每一步实际上会在一个动态长度的范围里做采样。。这样的优点是可以排除一些概率不高的单词,例如分布为[0.9,0.05,0.05]时,只考虑第一个就足够了,而top-k还是会考虑前k个。并且在分布相对均衡时,top-p会增加输出的多样性。 +4. 温度系数,在上面提到的归一化中,我们还可以引入温度系数调整概率分布 -为了预处理数据集,我们需要将文本转换为模型能够理解的数字,使用Dataset.map()方法 -```python -# example 是一个dict,对应数据集的每个元素,并返回一个包含input_ids、attention_mask 和token_type_ids为key的新dict -# 在机器学习任务中,一个example通常定义为模型的输入(也成为特征集合) -def tokenize_function(example): - return tokenizer(example["sentence1"], example["sentence2"], truncation=True) -tokenized_datasets = raw_datasets.map(tokenize_function, batched=True) -print(raw_datasets) -#DatasetDict({ -# train: Dataset({ -# features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'], -# num_rows: 3668 -# }) -# validation: Dataset({ -# features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'], -# num_rows: 408 -# }) -# test: Dataset({ -# features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'], -# num_rows: 1725 -# }) -#}) -``` -为了解决句子长度统一的问题,我们必须定义一个collate函数,该函数会将每个batch句子填充到正确的长度。 + $$ + Softmax(x_i) = \frac{e^{x_i}/T}{\sum_j e^{x_j}/T} + $$ -```python -from datasets import load_dataset -from transformers import AutoTokenizer, DataCollatorWithPadding + T越大归一化后的概率分布越均匀,而T越小概率分布越陡峭。因此大的T会增加模型输出的随机性,而小的T则会让模型的输出更加固定。这也是“温度系数影响模型创造性”说法的原因。 -raw_datasets = load_dataset("glue", "mrpc") -tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased") -def tokenize_function(example): - return tokenizer(example["sentence1"], example["sentence2"], truncation=True) -tokenized_datasets = raw_datasets.map(tokenize_function, batched=True) -data_collator = DataCollatorWithPadding(tokenizer=tokenizer) -``` -加载本地数据集 -```python -from datasets import load_dataset -#{ -# "data": [ -# { -# "title": "Terremoto del Sichuan del 2008", -# "paragraphs": [{...}] -# } -# ], -# "version": "1.1" -#} -squad_it_dataset = load_dataset("json", data_files="SQuAD_it-train.json", field="data") -print(squad_it_dataset) # 加载本地文件会创建一个带有train的DatasetDict 对象 -# DatasetDict({ -# train: Dataset({ -# features: ['title', 'paragraphs'], -# num_rows: 442 -# }) -#}) -data_files = {"train": "SQuAD_it-train.json", "test": "SQuAD_it-test.json"} -squad_it_dataset = load_dataset("json", data_files=data_files, field="data") -print(squad_it_dataset) # 包括 train 和 test 的 DatasetDict 对象 -#DatasetDict({ -# train: Dataset({ -# features: ['title', 'paragraphs'], -# num_rows: 442 -# }) -# test: Dataset({ -# features: ['title', 'paragraphs'], -# num_rows: 48 -# }) -#}) -``` - -与 Pandas 类似,transformers Datasets 提供了几个函数来操作 Dataset 和 DatasetDict 对象 -1. rename_column 重命名DatasetDict中的列 -2. filter 过滤一些行 -3. 为数据集中的所有行创建新的数据列 - ```python - def compute_review_length(example): - return {"review_length": len(example["review"].split())} - drug_dataset = drug_dataset.map(compute_review_length) - ``` -4. 用于预训练 GPT-2 的 WebText 语料库包含超过 800 万个文档和 40 GB 的文本,全加载到计算机内存吃不消,`pubmed_dataset_streamed = load_dataset("json", data_files=data_files, split="train", streaming=True)`,streaming=True 返回的对象是一个 IterableDataset +以上策略体现在代码上 就是:经过多个AttentionLayer hidden_states,Normalization之后得到 outputs.logits,将 logits 传递给 logits_processor 和 logits_warper(包含TopKLogitsWarper/TopPLogitsWarper等),最后,使用 softmax 函数将经过预处理的 logits 转换为概率分布,并利用 multinomial 方法从中采样得到下一个 token。 -## Trainer +## 训练/Trainer transformer 支持自定义dataset,自定义model实现forward(forward 支持的参数均可以作为dataset的column),forward 过程中还计算loss,模型的差异性基本已经兜住了,这也是为何 只要提供包含特定column的dataset,剩下的训练代码都可以交给trainer封装掉。 @@ -525,13 +550,13 @@ attention_mask = torch.ones(input_ids.shape, dtype=torch.int64) # predicts the next token at each position. 也就是 input_ids = [v1,v2,v3] 输出为 [v20,v30,v4]]。 v20 是根据v1 生成的下一个token,大概率跟v2 不一样,v4 是根据v1,v2,v3 生成的。 out = model(input_ids=input_ids, attention_mask=attention_mask) logits = out['logits'] -# -1 在python list 里表示最后一个元素 +# -1 在python list 里表示最后一个元素。 new_id = logits[:, -1, :].argmax(dim=1) print(new_id) print(tokenizer.batch_decode(new_id)) ``` -一次想预测一个字符太慢了 可以直接生成一段话,这也是model.generate 的原理。 +[GPT2 源码解析](https://zhuanlan.zhihu.com/p/630970209) 建议细读 ```python input_ids = enc['input_ids'] @@ -631,5 +656,5 @@ print('GENERATION=', tokenizer.batch_decode(out.cpu())) {"prompt":"orange is ","completion":"orange"} {"prompt":"sky is ","completion":"blue"} ``` -How exactly is GPT-3 trained on such examples? We are not exactly sure (OpenAI is very secretive), but perhaps the two sequences of tokens are concatenated together, then GPT-3 is trained on such examples, **but the loss is only calculated in the “completion” part**. PS: 终于知道为何要分成两段,而不是喂一个文本就算了。 +How exactly is GPT-3 trained on such examples? We are not exactly sure (OpenAI is very secretive), but perhaps the two sequences of tokens are concatenated together, then GPT-3 is trained on such examples, **but the loss is only calculated in the “completion” part**. PS: 终于知道为何要分成两段,而不是喂一个文本就算了。labels 中prompt部分的位置都置为-100,-100表示在计算loss的时候会被忽略,这个由任务性质决定。 diff --git a/_posts/MachineLearning/2023-10-30-from_attention_to_transformer.md b/_posts/MachineLearning/2023-10-30-from_attention_to_transformer.md index 06201a2c..f6beb6df 100644 --- a/_posts/MachineLearning/2023-10-30-from_attention_to_transformer.md +++ b/_posts/MachineLearning/2023-10-30-from_attention_to_transformer.md @@ -38,7 +38,7 @@ Transformer 模型本来是为了翻译任务而设计的。在训练过程中 ## self-attention 机制 -self-attention 机制用于计算句子中当前词与其他词的联系,举个例子: +在处理每个单词的表示时,要对你传递给它的句子中某些单词特别关注(并且忽略其他单词)。一个单词本身具有意义,但是该意义深受上下文影响,这可以是正在研究的单词之前或之后的任何其他单词(或多个)。self-attention 机制用于计算句子中当前词与其他词的联系,每个单词的新表示形式是由它自己和其他单词的交互得到的。举个例子: ``` The animal didn’t cross the street because it was too tired @@ -153,6 +153,8 @@ transformer中,模型输入encoder的每个token向量由两部分加和而成 ## Layer Normalization +这些都是用于Normalization激活的技术,可以加速学习,提高模型的性能。 + 在每个block中,最后出现的是Layer Normalization,其作用是规范优化空间,加速收敛。当我们使用梯度下降算法做优化时,我们可能会对输入数据进行归一化,但是经过网络层作用后,我们的数据已经不是归一化的了。随着网络层数的增加,数据分布不断发生变化,偏差越来越大,导致我们不得不使用更小的学习率来稳定梯度。Layer Normalization 的作用就是保证数据特征分布的稳定性,将数据标准化到ReLU激活函数的作用区域,可以使得激活函数更好的发挥作用 Normalization有两种方法,Batch Normalization和Layer Normalization。关于两者区别不再详述。 @@ -255,6 +257,45 @@ $$ 其中V就等价于$X^t$,而$softmax(\frac{QK^T}{\sqrt{d_k}})$就是我们想要学到的$W^t$(所以说self-attention 的权重是根据输入的改变和改变的,而CNN中一旦训练完成,各个通道的卷积核参数就固定了), attention 多做了一些什么呢?对于要学习的参数矩阵$W^t$增加了低秩的约束。参考 self-attention 的情况,即输出和输入的 size 保持一致,那么理论上没有任何约束的情况下$W^t$可以是一个满秩的n*n方阵。但是,在 self-attention 中,假设$Q,K\in\R^{n*d}$那么$softmax(\frac{QK^T}{\sqrt{d_k}})$就是一个秩至多为d的方阵。秩上的降低也意味着表征能力的下降,但好处在于$\mathcal{O}(n^2) $变成了$\mathcal{O}(nd) $, $d\ll n$ 的时候这样做的价值就体现出来了。比如原来比如GPT2 一个token向量 是768维,context若是4k,input item数量是768*4096,所以一定是预处理一下再接入mlp;cnn类似,一张1024*768的图片 每个pixel 作为一个item,weight就不小了。可以说attention 是一种为了压缩参数量而牺牲一些表征能力,是用类似于 matrix fatorization 的方式来近似转置线性层的做法。所以有人提出什么 **MLP is all you need** 之类的说法,用 MLP 替代 CNN 或者 attention 也能取得不俗的图像分类效果……那可不是必须的嘛?人家那可是加了各种约束,为了压缩参数量牺牲了表征能力来近似你这个线性层的结果啊。PS:大部分模型最后的部分都是mlp,所以可以视为前半部分的处理是在缩小input 规模以便接入mlp。 +### 代码 + +[以LLAMA为例,快速入门LLM的推理过程](https://mp.weixin.qq.com/s/5lbrqbqiHPZIARsVW6l6tA) + +``` +model = LlamaForCausalLM.from_pretrained(model, torch_dtype='auto') +print(model) +``` +可以通过print看模型结构: +``` +LlamaForCausalLM( + (model): LlamaModel( + (embed_tokens): Embedding(32000, 4096, padding_idx=31999) + (layers): ModuleList( + (0-31): 32 x LlamaDecoderLayer( + (self_attn): LlamaAttention( + (q_proj): Linear(in_features=4096, out_features=4096, bias=False) + (k_proj): Linear(in_features=4096, out_features=4096, bias=False) + (v_proj): Linear(in_features=4096, out_features=4096, bias=False) + (o_proj): Linear(in_features=4096, out_features=4096, bias=False) + (rotary_emb): LlamaRotaryEmbedding() + ) + (mlp): LlamaMLP( + (gate_proj): Linear(in_features=4096, out_features=11008, bias=False) + (down_proj): Linear(in_features=11008, out_features=4096, bias=False) + (up_proj): Linear(in_features=4096, out_features=11008, bias=False) + (act_fn): SiLUActivation() + ) + (input_layernorm): LlamaRMSNorm() + (post_attention_layernorm): LlamaRMSNorm() + ) + ) + (norm): LlamaRMSNorm() + ) + (lm_head): Linear(in_features=4096, out_features=32000, bias=False) +) +``` + +LLAMA属于Decoder-only models,有32个LlamaDecoderLayer,每个Decoder包含一个LlamaAttention和LlamaMLP,然后是LlamaRMSNorm和head部分,核心的结构是LlamaDecoderLayer。30B的话有60个LlamaDecoderLayer,30B和7B的差别也就是decoder的个数和decoder的不同配置。 ## 其它 diff --git a/_posts/MachineLearning/2023-12-16-llm_inference.md b/_posts/MachineLearning/2023-12-16-llm_inference.md index 950fd2ce..591cf71d 100644 --- a/_posts/MachineLearning/2023-12-16-llm_inference.md +++ b/_posts/MachineLearning/2023-12-16-llm_inference.md @@ -15,7 +15,6 @@ keywords: llm inference * TOC {:toc} - ## 思路 [大模型推理加速技术概要](https://mp.weixin.qq.com/s/kr5-QFhPXrUb7omTvJ-rDw)目前大模型推理加速技术栈大体可以分成三层(从低到高): @@ -31,16 +30,14 @@ keywords: llm inference 4. Batching 批量化:将多个请求合并处理,是提高性能的另外一个关键办法,这个能大幅提高性能的原因主要有两个:1. 合并请求可以增大代数运算的矩阵规模,而下层代数库处理越大的矩阵规模,相对性能越高。2. 合并请求可以减少对静态的模型参数矩阵的扫描次数,减少内存带宽消耗。 3. 大模型调度引擎,vLLM、TensorRT-LLM(原FasterTransformer)、llama.cpp等。大模型调度引擎是2022年开始新出现的一层抽象。为什么有了执行引擎还需要大模型调度引擎?主要是因为大家希望进一步优化推理性能,而大模型架构相对固定(Transformer架构及变形),通过专门针对大模型而不是更通用的神经网络进行推理优化,就可以利用大模型架构的特点和算法特性,来进一步提高性能。 1. KV Cache:这是fairseq等系统很早就开始有的基础方法,就是将transformer attention计算中的Key和Value张量集合缓存下来,避免每输出一个token都重复计算。 - 2. Iteration-level scheduling 迭代层调度:这是2022年Orca引入的方法(参考文献1),推理引擎默认都是按请求批量化,而LLM推理需要多次迭代进行自回归计算,所以按“迭代”为单位进行批量化,可以提高并行度和性能。 + 2. Iteration-level scheduling 迭代层调度:这是2022年Orca引入的方法,推理引擎默认都是按请求批量化,而LLM推理需要多次迭代进行自回归计算,所以按“迭代”为单位进行批量化,可以提高并行度和性能。 3. PagedAttention 分页注意力: 这是今年vLLM引入的方法(参考文献2),背后洞察是上面提到的KV cache占用大量GPU内存,一个13B模型每个输出token对应的KV张量,需要800KB,而最长输出长度2048个token的话,一个请求就需要1.6GB显存。因此vLLM引入类似操作系统中的分页机制,大幅减少了KV cache的碎片化,提高性能。 4. GPTQ量化。有一批研究专注于寻找更优的量化方法,llama.cpp支持近期发表的GPTQ(参考文献3),默认将模型量化到4比特,大幅提升性能且准确率下降很小。 5. Fused kernels等各类手工优化:很多时候,手打优化都是少不了的办法,llama.cpp短时间积累大量用户,就是因为项目作者不怕麻烦,快速积累了大量手工小优化,集腋成裘,形成领先的综合性能。 ![](/public/upload/machine/vllm_arch.jpg) -1. vLLM是一个开源的大模型推理加速框架,通过PagedAttention高效地管理attention中缓存的张量,实现了比HuggingFace Transformers高14-24倍的吞吐量,就像在操作系统中管理CPU虚拟内存一样 -2. NVIDIA FasterTransformer (FT) 是一个用于实现基于Transformer的神经网络推理的加速引擎。它包含Transformer块的高度优化版本的实现,其中包含编码器和解码器部分。使用此模块,您可以运行编码器-解码器架构模型(如:T5)、仅编码器架构模型(如:BERT)和仅解码器架构模型(如:GPT)的推理。FT框架是用C++/CUDA编写的,依赖于高度优化的 cuBLAS、cuBLASLt 和 cuSPARSELt 库,这使您可以在 GPU 上进行快速的 Transformer 推理。与 NVIDIA TensorRT 等其他编译器相比,FT 的最大特点是它支持以分布式方式进行 Transformer 大模型推理。在底层,节点间或节点内通信依赖于 MPI 、 NVIDIA NCCL、Gloo等。因此,使用FasterTransformer,您可以在多个 GPU 上以张量并行运行大型Transformer,以减少计算延迟。同时,TP 和 PP 可以结合在一起,在多 GPU 节点环境中运行具有数十亿、数万亿个参数的大型 Transformer 模型。 -3. DeepSpeed-MII 是 DeepSpeed 的一个新的开源 Python 库,旨在使模型不仅低延迟和低成本推理,而且还易于访问。 +## 影响因素 当前的生成式大模型的推理可以分为两个阶段:Context 阶段和 Generation 阶段。Context 阶段是批量计算输入的 Prompt,属于计算密集型。Generation 阶段是逐字生成下一个 Token,属于访存密集型,虽然每一轮 Generation 的计算量小于 Context 阶段,但是访存量相当。大模型推理主要面临三个挑战:输入输出变长、计算规模大、显存占用大,针对这些挑战当前有多种优化手段进行优化: 1. 服务层面,打破之前的 Batch 只能同时返回结果的限制,允许部分请求结束后插入新的请求。 @@ -48,13 +45,10 @@ keywords: llm inference 3. 显存方面,Generation 计算的访存密集型可以通过 Flash Attention 优化访存,也可以通过 Paged Attention 方法优化推理过程显存占用从而支持更大的吞吐。Paged Attention基本构建了一个类似于CPU内存管理的内存管理系统,以减少内存碎片并充分利用内存吞吐量 1. 对于较短的文本输入 (词元数小于 1024),推理的内存需求很大程度上取决于模型权重的大小。 - -## 影响因素 - [语言大模型推理性能工程:最佳实践](https://mp.weixin.qq.com/s/mniKrBWkDE1tWWb2wQBDCA) 我们应该如何准确衡量模型的推理速度呢?首个词元生成时间(Time To First Token,简称TTFT);单个输出词元的生成时间;时延:模型为用户生成完整响应所需的总时间;吞吐量:推理服务器在所有用户和请求中每秒可生成的输出词元数。 以下通用技术可用于优化语言大模型的推理: -1. 算子融合:将相邻的不同算子合并在一起通常可以获得更短的时延。 +1. 算子融合:将相邻的不同算子合并在一起通常可以获得更短的时延(避免了反复从HBM中读写数据)。 2. 量化:对激活值和权重进行压缩,以使用更少的比特数。一般来说,所有量化技术的工作原理如下: $Y=X*W$ 变成 $Y=X* dequantize(W); quantize(W)$,当输入向量走过模型计算图时,所有权重矩阵都会依次执行反量化和重量化操作。因此,使用权重量化时,推理时间通常 不会 减少,反而会增加。 3. 压缩:稀疏性或蒸馏。 4. 并行化:在多个设备间进行张量并行,或者针对较大的模型进行流水线并行。 @@ -63,6 +57,12 @@ keywords: llm inference 对于服务成本来说,推理硬件的利用率非常重要。由于GPU的价格十分高昂,因此我们需要尽可能地让它们完成更多工作。共享推理服务通过将多个用户的工作负载组合在一起,填补各自的差距,并将重叠的请求进行批处理,以降低成本。对于LLaMA2-70B等大型模型,只有在较大的批大小下才能实现更好的性价比。拥有能够以较大批大小运行的推理服务系统对于成本效率至关重要。然而,较大的批大小意味着较大的KV缓存,这反过来又增加了部署模型所需的GPU数量。我们需要在这两者之间博弈,进行取舍,共享服务运营商需要权衡成本,优化系统。 +KV Cache/PagedAttention/FlashAttention 本质是基于GPU计算和内存架构,对注意力计算过程的优化 + +$$ +Attention(Q,K,V) = softmax(\frac{QK^T}{\sqrt{d_k}})V +$$ + ### KV Cache 有许多针对Transformer的重要优化技术,如KV(键-值)缓存,每个Transformer层有一个KV缓存。但是有一个问题就是KV Cache非常的大,比如说拿LLaMA-13B举例子,假设每个token在一层的大小有20KB,LLaMA-13B有40层,这样这个token大小就会达到800KB,而一个sequence一般来说会有几千的token,也就是说一个sequence就会达到几个G。[Transformers KV Caching Explained](https://medium.com/@joaolages/kv-caching-explained-276520203249) 动图实在太贴切了。 @@ -93,10 +93,20 @@ Memory waste in KV Cache [如何解决LLM大语言模型的并发问题?](https://www.zhihu.com/question/613263140/answer/3271554389)vLLM:Efficient memory management for LLM inference 受到操作系统中的分页和虚拟内存的启发,将KV Block当做页,将Request当做进程,以此来实现vLLM。PagedAttention机制:传统要求将keys和values存到连续的内存空间,因为我们知道传统大家都是用TensorFlow、pytorch之类的,它是一个个tensor,所以很自然的就假设给它们一段连续的内存空间,但是对于LLM来说,这个假设就不是一个好的假设,因此PagedAttention允许在非连续内存空间中存储连续的keys和values,vLLM维护一个Block table,存放逻辑空间到物理空间的映射。现在有一个Prompt:Alan Turing is a computer scientist,当产生一个新的token时,会查看Block table中的Physical block no.,然后找到对应物理内存的地方存储进去,并更新Block table中的Filled slots内容。当产生“renowned”的时候,是新开了一个Block,所以也要更新Block table,新开一个物理内存(每个kv block中有固定的token数目)。 +PS:Transformer (和Attention) layer 已经支持了缓存机制 (use_cache=true),kvcache 在代码上如何体现可以理解。pageattention 是不是可以理解为:pageattention 初始化了cache,只要把这个cache 引用传给 Transformer (和Attention) forward 函数参数,Transformer 就可以用这个cache 到计算过程中了? + +### 调度优化 + +Batching就是将一段时间内到达的用户请求合并到一起,提交到GPU中执行,从而提高系统的吞吐量。然而,**与传统的 DNN Model 在推理时只要正向执行一遍不同,基于 Transformer 的 Generative Model 在推理时是迭代式的(Iterative),每个请求都需要迭代式执行多次,每次生成部分结果(一个 Token),且每个请求的迭代次数可能是不同的(例如迭代直到模型生成一个 End-Of-Sequence Token)**。因此将现有的 Batching 方式应用在 Generative Model 时,可能导致有的请求已经迭代结束了,但是还需要和同Batch中没有迭代结束的请求继续一起执行。这个问题的核心在于,传统的 Batching 技术是以 Request 为粒度的,将多个 Request 绑定在一起提交给执行引擎,多个 Request 同时开始同时结束。因此需要一个新的 Batching 的方式,这也是本项工作核心的 Insight:使用更细粒度的,Iteration-level Batching,在每个 Iteration 中将不同的 Request 合并到一起。 +### 推理时的模型并行(未完成) ## 模型服务框架 +1. vLLM是一个开源的大模型推理加速框架,通过PagedAttention高效地管理attention中缓存的张量,实现了比HuggingFace Transformers高14-24倍的吞吐量,就像在操作系统中管理CPU虚拟内存一样 +2. NVIDIA FasterTransformer (FT) 是一个用于实现基于Transformer的神经网络推理的加速引擎。它包含Transformer块的高度优化版本的实现,其中包含编码器和解码器部分。使用此模块,您可以运行编码器-解码器架构模型(如:T5)、仅编码器架构模型(如:BERT)和仅解码器架构模型(如:GPT)的推理。FT框架是用C++/CUDA编写的,依赖于高度优化的 cuBLAS、cuBLASLt 和 cuSPARSELt 库,这使您可以在 GPU 上进行快速的 Transformer 推理。与 NVIDIA TensorRT 等其他编译器相比,FT 的最大特点是它支持以分布式方式进行 Transformer 大模型推理。在底层,节点间或节点内通信依赖于 MPI 、 NVIDIA NCCL、Gloo等。因此,使用FasterTransformer,您可以在多个 GPU 上以张量并行运行大型Transformer,以减少计算延迟。同时,TP 和 PP 可以结合在一起,在多 GPU 节点环境中运行具有数十亿、数万亿个参数的大型 Transformer 模型。 +3. DeepSpeed-MII 是 DeepSpeed 的一个新的开源 Python 库,旨在使模型不仅低延迟和低成本推理,而且还易于访问。 + 使用大模型时,我们在huggingface或modelscope 看到的代码类似下面,很明显不能直接向用户提供服务。 ```python from transformers import AutoTokenizer, AutoModel @@ -313,6 +323,7 @@ GPU编程基础:在执行model.generate(prompt)时,我们进行以下操作 2. 这一步骤受内存限制,因为我们仅计算一个词元,未充分利用SM。 +函数计算推出 GPU 闲置计费功能,在保障性能的前提下,可以帮助您大幅降低 GPU 的成本开销。以往部署大型语言模型(LLM)可能需要昂贵的 GPU 支持,尤其在需要大量计算资源时。但请求处理并不是每时每刻都处于活跃状态,势必存在流量的潮汐现象,后端的计算资源会出现空载导致成本的浪费。借助函数计算 GPU 闲置计费功能,用户的开销将会根据实际计算负载动态调整。 ### 在线推理 diff --git a/_posts/MachineLearning/2023-12-16-llm_train.md b/_posts/MachineLearning/2023-12-16-llm_train.md index a1cfeaf4..75788a93 100644 --- a/_posts/MachineLearning/2023-12-16-llm_train.md +++ b/_posts/MachineLearning/2023-12-16-llm_train.md @@ -77,7 +77,7 @@ DeepSpeed 实现的 ZeRO ,出发点是为了减少显存使用,跨机器跨 ![](/public/upload/machine/gpu_memory_usage.jpg) -在DeepSpeed下,ZeRO训练支持了完整的ZeRO Stages1, 2和3,以及支持将优化器状态、梯度和模型参数从GPU显存下沉到CPU内存或者硬盘上,实现不同程度的显存节省,以便训练更大的模型。不同Stage对应的做法: +在DeepSpeed下,ZeRO训练支持了完整的ZeRO Stages1, 2和3,以及支持将优化器状态、梯度和模型参数**从GPU显存下沉到CPU内存或者硬盘上**,实现不同程度的显存节省,以便训练更大的模型。不同Stage对应的做法: - Stage 1: 把 优化器状态(optimizer states) 分片到每个数据并行的工作进程(每个GPU)下 - Stage 2: 把 优化器状态(optimizer states) + 梯度(gradients) 分片到每个数据并行的工作进程(每个GPU)下 - Stage 3: 把 优化器状态(optimizer states) + 梯度(gradients) + 模型参数(parameters) 分片到每个数据并行的工作进程(每个GPU)下 diff --git a/_posts/Technology/Go/2022-12-09-go_performance.md b/_posts/Technology/Go/2022-12-09-go_performance.md index 522cee59..0d2316ff 100644 --- a/_posts/Technology/Go/2022-12-09-go_performance.md +++ b/_posts/Technology/Go/2022-12-09-go_performance.md @@ -24,9 +24,7 @@ keywords: Go io ## Profiling -pprof 是用于可视化和分析性能分析数据的工具。 - -有两种类型的 profiler : +pprof 是用于可视化和分析性能分析数据的工具。为什么pprof可以帮助我们分析Go程序性能呢?因为它可以采集程序运行时数据:比如说协程栈,这样服务阻塞在哪里是不是一目了然了;比如说内存分配情况包括调用栈,这样哪里耗费内存也清楚了。有两种类型的 profiler : 1. 追踪型:任何时候触发提前设定的事件就会做测量,例如:函数调用,函数退出,等等 2. 采样型:常规时间间隔做一次测量 diff --git a/_posts/Technology/Monitor/2021-01-13-observability.md b/_posts/Technology/Monitor/2021-01-13-observability.md index 14d694ea..4bbb9498 100644 --- a/_posts/Technology/Monitor/2021-01-13-observability.md +++ b/_posts/Technology/Monitor/2021-01-13-observability.md @@ -213,6 +213,8 @@ devops基本理念: [AIOps在美团的探索与实践——事件管理篇](https://mp.weixin.qq.com/s/9U7PKSt60AbRx7Ngud0Qxg) 未细看 +[如何利用 AI 算法解决告警配置三大难题?](https://mp.weixin.qq.com/s/6WLb8cVK3Z7qoGsD_7ht5A) 有没有合适工具,告诉小 A 应该对哪些指标配告警?有没有合适工具,给小 A 自动推荐合适的告警阈值?有没有合适工具,帮小 A 给起伏不定的指标配告警? + ## 其它 [做出让人爱不释手的基础软件:可观测性和可交互性](https://mp.weixin.qq.com/s/WEO1y8vg21CXlix8wO28hw) 个人理解: 尽可能多的监控,之后就是尽量串起来 diff --git a/public/upload/kubernetes/gpu_arch.png b/public/upload/kubernetes/gpu_arch.png deleted file mode 100644 index bcf4ba08..00000000 Binary files a/public/upload/kubernetes/gpu_arch.png and /dev/null differ diff --git a/public/upload/machine/gpu_arch.jpg b/public/upload/machine/gpu_arch.jpg new file mode 100644 index 00000000..3b15765b Binary files /dev/null and b/public/upload/machine/gpu_arch.jpg differ