From cea3a6664f75818c9d19189ff425d74e8b6b0c05 Mon Sep 17 00:00:00 2001 From: "liqiankun.1111" Date: Fri, 15 Nov 2024 13:56:16 +0800 Subject: [PATCH] add ml --- .../2019-03-03-kubernetes_scheduler.md | 8 ++++++++ .../2020-03-21-resource_scheduler.md | 9 +++++---- .../Framework/2023-12-16-llm_inference.md | 16 ++++++++++------ .../MachineLearning/Infra/2021-08-18-gpu.md | 8 ++++++-- .../Infra/2024-09-28-inference_tmp.md | 2 ++ .../LLM/2023-09-07-multimodal_llm.md | 2 ++ .../LLM/2023-09-25-llm_retrieval.md | 1 + .../LLM/2023-10-30-llm_agent.md | 5 +++-- .../LLM/2024-06-10-multi_agent.md | 4 ++++ .../Model/2019-08-31-mathematical_interest.md | 5 +++++ .../Model/2021-10-31-from_rnn_to_attention.md | 4 ++++ ...023-10-30-from_attention_to_transformer.md | 16 +++++++++------- .../MachineLearning/Model/2024-10-11-bert.md | 2 +- .../Algorithm/2016-03-15-data_structure.md | 4 +++- .../2018-10-01-object_oriented.md | 13 +++++++++++++ .../Architecture/2019-08-14-complexity.md | 2 +- .../2019-08-20-clean_architecture_note.md | 2 ++ _posts/Technology/DDD/2017-12-25-ddd.md | 19 ++++++++++++++++++- .../Technology/DDD/2018-11-13-crud_defect.md | 11 ++++++++++- .../Technology/DDD/2023-07-06-ddd_practice.md | 7 +++++++ .../Technology/Python/2024-05-19-python_vm.md | 12 ++++++++++++ _posts/Technology/Storage/2024-10-12-neo4j.md | 7 ++++++- 22 files changed, 132 insertions(+), 27 deletions(-) diff --git a/_posts/Kubernetes/Scheduler/2019-03-03-kubernetes_scheduler.md b/_posts/Kubernetes/Scheduler/2019-03-03-kubernetes_scheduler.md index 1db3d8b0..62cf5d2c 100644 --- a/_posts/Kubernetes/Scheduler/2019-03-03-kubernetes_scheduler.md +++ b/_posts/Kubernetes/Scheduler/2019-03-03-kubernetes_scheduler.md @@ -347,3 +347,11 @@ func (f *frameworkImpl) RunPreFilterPlugins(ctx context.Context, state *framewor return nil } ``` + +## 其它 +[6 张图带你深入了解 kube-scheduler](https://mp.weixin.qq.com/s/2elOZD0yaBf-WvMCSD5zHQ)Scheduler 的作用是 负责将 Pod 调度到 Node 上。如果让你设计这个组件,你会如何设计,保证它稳定高效的运行。 +1. 需要能够实时监听到 有新的 Pod 待调度 +2. 同一时间如果有大量待调度的 Pod,如果处理,如何保证不能漏掉,应该先处理哪个 Pod,调度过程中,如果失败,如何处理。所以得加个队列,有重试机制等 +3. 调度过程中依赖 Node、Pod 的实时信息,根据 Node、Pod 信息,决策 Pod 调度到哪个Node上合适,每次调度 调 Apiserver ,显然低效, 得在本地缓存一份数据,加个缓存 +4. 调度选择过程中,考虑因素太多,很难周全,可扩展性一定要设计好 +5. Pod 绑定过程中 可能依赖 pvc 绑定等,耗时较长, 所以绑定得是异步的, 但是匹配哪个Node合适的算法 需要同步执行,所以要有两个周期, 调度周期和绑定周期,调度周期串行,绑定周期并行 \ No newline at end of file diff --git a/_posts/Kubernetes/Scheduler/2020-03-21-resource_scheduler.md b/_posts/Kubernetes/Scheduler/2020-03-21-resource_scheduler.md index 779ea4c6..d6f4ab75 100644 --- a/_posts/Kubernetes/Scheduler/2020-03-21-resource_scheduler.md +++ b/_posts/Kubernetes/Scheduler/2020-03-21-resource_scheduler.md @@ -61,10 +61,11 @@ Kubernetes和Mesos调度的最大区别在于资源调度请求的方式 [揭开阿里巴巴复杂任务资源混合调度技术面纱](https://mp.weixin.qq.com/s/wj0hrHIRHnBUNZZXnRm5xg) -业界系统的调度架构主要分为四类:单体调度、两层调度、共享调度和混合调度。这些调度架构的本质区别其实只有两点: -1. 调度时调度器是否拥有全局的资源视图; -2. 不同的应用是否拥有多个资源调度器。 -单体调度顾名思义,即只有一个调度器,调度器拥有全局资源视图的架构,Google Borg 和 K8s 都采用这个架构。单体架构的好处是,所有的任务都由唯一的调度器处理,调度器可以充分的考虑全局的资源使用情况,能方便的做出最优调度。但由于调度架构的限制,集群性能受限于单体的性能,无法支撑过大的集群。两层调度拥有多个调度器,Apache Mesos 和 Hadoop YARN 都采用这个架构。两层调度中,每个应用的调度器首先向中心节点获取资源,再将其分配给应用中的各个任务。两层调度解决了单体调度的性能问题,但是调度器仅拥有局部资源视图,无法做出最优调度。共享调度拥有多个调度器,每个调度器拥有全局资源视图,Omega 采用了这个架构。共享调度方案中,每个调度器都可以并发地从整个资源池中申请资源,解决了性能问题和最优调度问题,且可以支持较大集群。 +业界系统的调度架构主要分为四类:单体调度、两层调度、共享调度和混合调度。这些调度架构的本质区别其实只有两点:调度时调度器是否拥有全局的资源视图;不同的应用是否拥有多个资源调度器。 +1. 单体调度顾名思义,即只有一个调度器,调度器拥有全局资源视图的架构,Google Borg 和 K8s 都采用这个架构。单体架构的好处是,所有的任务都由唯一的调度器处理,调度器可以充分的考虑全局的资源使用情况,能方便的做出最优调度。但由于调度架构的限制,集群性能受限于单体的性能,无法支撑过大的集群。 +2. 两层调度拥有多个调度器,Apache Mesos 和 Hadoop YARN 都采用这个架构。两层调度中,每个应用的调度器首先向中心节点获取资源,再将其分配给应用中的各个任务。两层调度解决了单体调度的性能问题,但是调度器仅拥有局部资源视图,无法做出最优调度。 +3. 共享调度拥有多个调度器,每个调度器拥有全局资源视图,Omega 采用了这个架构。共享调度方案中,每个调度器都可以并发地从整个资源池中申请资源,解决了性能问题和最优调度问题,且可以支持较大集群。调度器间资源申请冲突可通过悲观锁或乐观锁来解决。 + ## 资源调度的复杂性 [没有银弹,只有取舍 - Serverless Kubernetes 的思考与征程(一)](https://mp.weixin.qq.com/s/1aMalQs-AE2L1aA5X20gJA) diff --git a/_posts/MachineLearning/Framework/2023-12-16-llm_inference.md b/_posts/MachineLearning/Framework/2023-12-16-llm_inference.md index 24885d34..97653e2e 100644 --- a/_posts/MachineLearning/Framework/2023-12-16-llm_inference.md +++ b/_posts/MachineLearning/Framework/2023-12-16-llm_inference.md @@ -180,7 +180,7 @@ LLM推理需要的芯片形态,最重要的是内存带宽和互联带宽, ![](/public/upload/machine/attention_kvcache.png) -在推理的时候transformer本质上只需要计算出$O_i$ ,即一个字一个字地蹦。PS:也回答了为何不缓存Q +在推理的时候transformer本质上只需要计算出$O_i$ ,即一个字一个字地蹦。PS:也回答了为何不缓存Q?每一轮的q都是新的。 1. Attention的第i个输出只和第 i 个query有关,和其他query无关,所以query完全没有必要缓存,每次预测 $O_i$时只要计算最新的$O_i$,其他的丢弃即可。 2. Attention的输出$O_i$的计算和完整的K和V有关,而K、V的历史值只和历史的O有关,和当前的O无关。那么就可以通过缓存历史的K、V,而避免重复计算历史K、V ![](/public/upload/machine/kvcache_no_query.png) @@ -236,6 +236,13 @@ System Prompt Caching,也称为 Prefix Sharing,其基本思想是对System P ### Flash Attention + +[Flash Attention 的数学原理](https://mp.weixin.qq.com/s/Nv9iS96J7pVZRvH7U8fWsA)**Flash Attention 并没有减少 Attention 的计算量**,也不影响精度,但是却比标准的Attention运算快 2~4 倍的运行速度,减少了 5~20 倍的内存使用量。究竟是怎么实现的呢?简单来说,Flash Attention 让 Attention 的所有计算都符合结合律,这样就可以充分利用 GPU 的并行优势。从Attention 计算公式可以看出,Attention 的并行计算主要解决两个问题: +1. 矩阵的并行计算。$X=QK^T$ +2. Softmax 的并行计算。$A=softmax(\frac{X}{\sqrt{d}})$。 +矩阵的并行算法已经比较成熟,GPU 上也有 TensorCore 来加速计算。 Softmax 操作让并行计算有些棘手。Softmax 的并行无法将计算放到一个 Kernel 中。后面的建议细读文章。 +传统方法计算时,需要引入两个中间矩阵X和A并存在全局内存中。需要先从全局内存中读取矩阵 QK,并将计算好的X写入全局内存。然后从全局内存中读取X,计算Softmax得到矩阵A,再将其写入全局内存,最后读取矩阵A和矩阵V,计算$O=AV$ 计算得到矩阵O,这个过程会极大的占用显存的带宽。Flash Attention 的目标是尽可能使用 SRAM来加快计算速度,避免从全局内存中读取或写入注意力矩阵(H100 全局内存80G,访问速度3.35TB/s,但当全部线程同时访问全局内存时,其平均带宽仍然很低)。达成该目标需要做到在不访问整个输入的情况下计算softmax函数,并且后向传播中不能存储中间注意力矩阵(存部分信息,反向传播时重新计算)。PS:通过数学变换,换个算法,减少内存占用,pytorch2.0 已支持Flash Attention。PS:FA的本质是融合算子的一种新的实现方式。 + [图解大模型计算加速系列:Flash Attention V1,从硬件到计算逻辑](https://mp.weixin.qq.com/s/J2i2MDv4us_GMwCyku0tnw)在矩阵分块的时候设计好一个能够充分利用高速访存HBM的分块方法,让一次搬运进HBM中的参数可以全部和该做乘加操作的算子都计算完再丢弃,达到数量单次访存利用率最大化。 1. Fast(with IO-Awareness),计算快。它发现:计算慢的卡点不在运算能力,而是在读写速度上。所以它通过**降低对显存(HBM)的访问次数**来加快整体运算速度(通过分块计算(tiling)和核函数融合(kernel fusion)来降低对显存的访问),这种方法又被称为IO-Awareness。 2. Memory Efficicent,节省显存。在标准attention场景中,forward时我们会计算并保存N*N大小的注意力矩阵;在backward时我们又会读取它做梯度计算,这就给硬件造成了的存储压力。在Flash Attention中,则巧妙避开了这点,使得存储压力降至。在后文中我们会详细看这个trick。 @@ -245,11 +252,7 @@ System Prompt Caching,也称为 Prefix Sharing,其基本思想是对System P [图解大模型计算加速系列:Flash Attention V2,从原理到并行计算](https://mp.weixin.qq.com/s/gMRZV-ZCrFccKPKSkOpxsQ) 未读。 -[Flash Attention 的数学原理](https://mp.weixin.qq.com/s/Nv9iS96J7pVZRvH7U8fWsA)Flash Attention 并没有减少 Attention 的计算量,也不影响精度,但是却比标准的Attention运算快 2~4 倍的运行速度,减少了 5~20 倍的内存使用量。究竟是怎么实现的呢?简单来说,Flash Attention 让 Attention 的所有计算都符合结合律,这样就可以充分利用 GPU 的并行优势。从Attention 计算公式可以看出,Attention 的并行计算主要解决两个问题: -1. 矩阵的并行计算。$X=QK^T$ -2. Softmax 的并行计算。$A=softmax(\frac{X}{\sqrt{d}})$。 -矩阵的并行算法已经比较成熟,GPU 上也有 TensorCore 来加速计算。 Softmax 操作让并行计算有些棘手。Softmax 的并行无法将计算放到一个 Kernel 中。后面的建议细读文章。 -传统方法计算时,需要引入两个中间矩阵X和A并存在全局内存中。需要先从全局内存中读取矩阵 QK,并将计算好的X写入全局内存。然后从全局内存中读取X,计算Softmax得到矩阵A,再将其写入全局内存,最后读取矩阵A和矩阵V,计算$O=AV$ 计算得到矩阵O,这个过程会极大的占用显存的带宽。Flash Attention 的目标是尽可能使用 SRAM来加快计算速度,避免从全局内存中读取或写入注意力矩阵(H100 全局内存80G,访问速度3.35TB/s,但当全部线程同时访问全局内存时,其平均带宽仍然很低)。达成该目标需要做到在不访问整个输入的情况下计算softmax函数,并且后向传播中不能存储中间注意力矩阵(存部分信息,反向传播时重新计算)。PS:通过数学变换,换个算法,减少内存占用,pytorch2.0 已支持Flash Attention。PS:FA的本质是融合算子的一种新的实现方式。 +[FlashAttention算法之美:极简推导版](https://mp.weixin.qq.com/s/hu5D1dmCFkeStxbXBE-czA) 未读。 ### 调度优化/动态批处理 @@ -362,6 +365,7 @@ FasterTransformer 是真对于 Transofrmer 类型模型(也包括 encoder-only 2. 量化对于文本生成特别有效,因为我们关心的是选择 最可能的下一个词元的分布 ,而不真正关心下一个词元的确切 logit 值。所以,只要下一个词元 logit 大小顺序保持相同, argmax 或 topk 操作的结果就会相同。 3. 常用量化方法:GPTQ、AWQ和GGUF 3. 模型稀疏化。模型稀疏化是一种重要的优化方法。它的主要目的是减少模型参数的数量,从而降低模型的复杂度,提高模型的泛化能力和计算效率。模型稀疏化的主要方法有剪枝、量化、低秩近似等。剪枝是一种直接删除模型中部分参数的方法,它可以有效地减少模型的规模,但需要注意不能过度剪枝,以免影响模型的性能。低秩近似则是通过将模型转换为低秩矩阵,来减少模型的参数数量。 +4. 推理引擎都是做成多卡TP而不是PP,主要是因为从服务器视角看PP的吞吐上限更高,但是从单个请求视角看TP的延迟会更低,在线服务往往不需要那么高的吞吐,延迟更加重要。后来vLLM还增加了流水线并行(Pipeline Parallelism)的支持,从vLLM版本 0.5.1 开始支持跨多节点的流水线并行,对于那些跨多个节点的超大模型和低带宽连接,流水线并行是一种更优的选择。 ## 硬件 diff --git a/_posts/MachineLearning/Infra/2021-08-18-gpu.md b/_posts/MachineLearning/Infra/2021-08-18-gpu.md index 13c5e2fb..e8d31496 100644 --- a/_posts/MachineLearning/Infra/2021-08-18-gpu.md +++ b/_posts/MachineLearning/Infra/2021-08-18-gpu.md @@ -17,6 +17,10 @@ keywords: gpu GPU 的架构;内存管理;任务管理;数据类型。 +内存将数据传输到CPU大概每秒大概传输200GB(也就是每秒25G FP64),cpu 计算能力大概是2000 GFLOPs FP64,这两者的比值就是设备的计算强度。也就是cpu 每秒不对一个数据处理80次,cpu 就会空闲,但也没有什么算法需要对一个数据处理80次。当增加FLOPs速度比增加内存带宽的速度快的时候,计算强度就会上升。为什么?物理上光速 3亿米/秒,电脑时钟30亿Hz,在一个时钟周期,光只传播了10cm,电流在硅中的传播速度只有光的五分之一(6万公里/秒),物理实际上很复杂,根据经验,一个时钟周期内,电流的移动只有20mm。 +CPU 的期望是一个线程基本完成所有工作,将这些线程从一个切换到另一个是非常昂贵的(上下文切换),cpu设计者把所有资源都投入到延迟上了。GPU 设计师将所有资源的都投入到增加线程中,而不是减少延迟。此外,gpu 使用寄存器缓存来解决高延迟问题,以及通过靠近数据来减少延迟(内存传输数据慢,cpu 没有忙起来,内存也没有忙起来)。gpu 选择增加线程,每个sm2048个线程,一次跑一批(warp)线程,当一些线程因为等待延迟关闭时,其它线程大概已经load好数据了准备运行了。这就是gpu工作的秘密,它可以在不同的warp之间切换,并且在一个时钟周期内完成,所以根本没有上下文开销。gpu 是一个吞吐量系统,总是超量分配线程,超量分配意味着总是在内存快的(指的是数据ready?)时候工作。(a grid represents all work to be done)所有要做的工作都被分解成线程块(the grid comprises many blocks with an equal number of threads),每个块都有并行线程,保证线程同时运行,这样它们就可以共享数据(threads within a block run independently but may synchrionze to exchange data)(机器学习有一些计算,比如all-to-all)。但所有的块都是在超量分配模式下独立调度的,这样才能两全其美。但它也允许一定数量的线程相互交互,这就是gpu编程的本质。PS:是不是可以认为,内存延迟高,这时为了cpu和内存提速(都别闲着),做大带宽,比如虽然10s传一次数据,但一次传10M,gpu 准备大量线程同时干活儿。后续有数据就干活儿,没干活儿就让位给另一批数据ready的线程。 + + ## GPU 各种游戏里面的人物的脸,并不是那个相机或者摄像头拍出来的,而是通过多边形建模(Polygon Modeling)创建出来的。而实际这些人物在画面里面的移动、动作,乃至根据光线发生的变化,都是通过计算机根据图形学的各种计算,实时渲染出来的。 @@ -86,7 +90,7 @@ GPU架构总体如下图所示: **两级线程层次结构**(带上grid也有说三层的,比较新的Hooper 架构 引入了Thread Block Clusters 层次),可以分为两个粒度来看 GPU: 1. 以SM 为基本单元来看GPU 整体架构,GPU由多个SM组成,而在SM之外,仅仅有global memory和L2 cache两个组件。PS:gpu sm 更类似于cpu 里的core,不同sm执行不同的指令单元 -2. SM的硬件架构:核心组件包括内存、计算单元和指令调度。每个SM包含多个核心,它们共享一个指令单元,但能够并行执行不同的线程。每个SM中的共享内存允许线程之间进行有效的数据交换和同步 +2. SM的硬件架构:核心组件包括内存、计算单元和指令调度。每个SM包含多个核心,它们共享一个指令单元,但能够并行执行不同的线程。每个SM中的共享内存允许线程之间进行有效的数据交换和同步。warp是gpu的基本调度单位。在一个是时钟周期内可以多个warp,一个sm包含64个warp,4个warps可以并行运行。 流式多处理器(Streaming Multiprocessor、SM)是 GPU 的基本单元,每个 GPU 都由一组 SM 构成,SM 中最重要的结构就是计算核心 Core 1. 线程调度器(Warp Scheduler):线程束(Warp)是最基本的单元,每个线程束中包含 32 个并行的线程,GPU 控制部件面积比较小,为了节约控制器,**一个 Warp 内部的所有 CUDA Core 的 PC(程序计数器)一直是同步的,但是访存地址是可以不同的,每个核心还可以有自己独立的寄存器组,它们使用不同的数据执行相同的命令**,这种执行方式叫做 SIMT(Single Instruction Multi Trhead)。调度器会负责这些线程的调度; @@ -420,7 +424,7 @@ GPU存储可分为物理内存(硬件真实存在的)和逻辑内存(由cu Naive GEMM的代码见下(完整代码见 sgemm_naive.cu ): -``` +```c // 将二维数组的行列索引转成一维数组的行列索引,这样可以更高效访问数据 // row, col:二维数组实际的行列索引,ld表示该数组实际的列数 // 例:二维数组实际的行列索引为(1, 3),即第二行第四个元素,二维数据的总列数 = 5 diff --git a/_posts/MachineLearning/Infra/2024-09-28-inference_tmp.md b/_posts/MachineLearning/Infra/2024-09-28-inference_tmp.md index ea706b7a..3cef4f21 100644 --- a/_posts/MachineLearning/Infra/2024-09-28-inference_tmp.md +++ b/_posts/MachineLearning/Infra/2024-09-28-inference_tmp.md @@ -23,6 +23,8 @@ Mooncake 采用了以 KVCache 为中心的分离式推理架构,主要由三 2. Decoding 池:这个部分集中处理所有解码阶段的任务。 3. KVCache 池:这个部分负责存储所有中间过程中应用到的 KVCache,并决定何时使用这些缓存,何时释放它们。 +prefill-decode 分离架构核心是解决Continous Batching在decode中会被插入prefill从而导致decode卡顿以及decode阶段MFU低下这两个问题。 + Context Caching ![](/public/upload/machine/context_caching.jpg) diff --git a/_posts/MachineLearning/LLM/2023-09-07-multimodal_llm.md b/_posts/MachineLearning/LLM/2023-09-07-multimodal_llm.md index ae1ef664..0a294e70 100644 --- a/_posts/MachineLearning/LLM/2023-09-07-multimodal_llm.md +++ b/_posts/MachineLearning/LLM/2023-09-07-multimodal_llm.md @@ -180,5 +180,7 @@ curl  https://api.openai-hk.com/v1/images/generations \ 多模态RAG的想法是允许RAG系统以某种方式将多种形式的信息注入到多模态模型中。因此,多模态RAG系统可能检索基于用户提示的文本、图像、视频和其他不同模态的数据,而不仅仅是检索文本片段。 有三种流行的方法可以实现多模态RAG。 1. 使用一个嵌入空间,检索出所有模态中与用户查询最相似的数据。 2. 将所有数据模态转换为单一模态,通常是文本。 + 1. 图片 ==> vlm ==> 文本 ==> 文本embedding + 2. 图片 ==> vemb ==> embedding。 生成时 一般也是将 query + topk emb 图片 + 其它文本 传给vlm 生成答案。 [训练VLM(视觉语言模型)的经验](https://zhuanlan.zhihu.com/p/890327005) 未读。 \ No newline at end of file diff --git a/_posts/MachineLearning/LLM/2023-09-25-llm_retrieval.md b/_posts/MachineLearning/LLM/2023-09-25-llm_retrieval.md index 8494ae68..e8a55ad1 100644 --- a/_posts/MachineLearning/LLM/2023-09-25-llm_retrieval.md +++ b/_posts/MachineLearning/LLM/2023-09-25-llm_retrieval.md @@ -157,6 +157,7 @@ LLM 擅长于一般的语言理解与推理,而不是某个具体的知识点 2. 有监督方案 ![](/public/upload/machine/supervised_learning_keyword_extraction.jpg) 6. 术语字典。用户输入术语字典,查询时先根据query查询术语字典,若有匹配的则将query中的术语替换/增强一下,再去检索知识库。 + 7. 把知识图谱的查询结果作为向量查询的输入信息,通过向量查询获取相关文档后,由LLM最终生成更加完整的问答结果。 2. Finetune 向量模型。在专业数据领域上,嵌入模型的表现不如 BM25,但是微调可以大大提升效果。embedding 模型 可能从未见过你文档的内容,也许你的文档的相似词也没有经过训练。在一些专业领域,通用的向量模型可能无法很好的理解一些专有词汇,所以不能保证召回的内容就非常准确,不准确则导致LLM回答容易产生幻觉(简而言之就是胡说八道)。可以通过 Prompt 暗示 LLM 可能没有相关信息,则会大大减少 LLM 幻觉的问题,实现更好的拒答。 3. 许多vdb支持了对元数据的操作。LangChain 的 Document 对象中有个 2 个属性,分别是page_content和metadata,metadata就是元数据,我们可以使用metadata属性来过滤掉不符合条件的Document。元数据过滤的方法虽然有用,但需要我们手动来指定过滤条件,我们更希望让 LLM 帮我们自动过滤掉不符合条件的文档。SelfQueryRetriever 4. **增加追问机制**。这里是通过Prompt就可以实现的功能,只要在Prompt中加入“如果无法从背景知识回答用户的问题,则根据背景知识内容,对用户进行追问,问题限制在3个以内”。这个机制并没有什么技术含量,主要依靠大模型的能力。不过大大改善了用户体验,用户在多轮引导中逐步明确了自己的问题,从而能够得到合适的答案。 diff --git a/_posts/MachineLearning/LLM/2023-10-30-llm_agent.md b/_posts/MachineLearning/LLM/2023-10-30-llm_agent.md index cb920fcd..aba8edf2 100644 --- a/_posts/MachineLearning/LLM/2023-10-30-llm_agent.md +++ b/_posts/MachineLearning/LLM/2023-10-30-llm_agent.md @@ -97,8 +97,8 @@ ReACT框架的一个关键特点是其任务拆解模块,能够将复杂的任 2. 有反馈的计划制定。这种范式允许LLMs与工具进行迭代交互,不预先承诺一个完整的任务计划。相反,它允许基于工具的反馈逐步调整子任务,使LLMs能够一步步地解决问题,并根据工具返回的结果不断完善计划。这种方法增强了LLMs的问题解决能力,因为它允许模型在响应工具反馈时进行适应和学习。反馈的提供者来自三个方面:环境反馈、人类反馈和模型反馈。 ReAct模式最早出现的Agent设计模式,目前也是应用最广泛的。从ReAct出发,有两条发展路线: -1. 一条更偏重Agent的规划能力,包括REWOO、Plan & Execute、LLM Compiler。 -2. 一条更偏重反思能力,包括Basic Reflection、Reflexion、Self Discover、LATS。 +1. 一条更偏重Agent的规划能力,包括REWOO、Plan & Execute、LLM Compiler。在重规划的模式下,ReAct模式加上规划器就成为REWOO,再加上重规划器就成为Plan & Execute,再叠加计划并行执行能力就成为LLM Compiler。 +2. 一条更偏重反思能力,包括Basic Reflection、Reflexion、Self Discover、LATS。在重反思模式下,ReAct模式加上左右互搏框架就成为Basic Reflecion,边推理边执行则演变为Self-Discover,加入强化学习则演变为Reflexion,最后的LATS是推理和规划的集大成者,LATS = Tree search + ReAct + Plan&Execute + Reflexion。 ![](/public/upload/machine/agent_plan.jpg) @@ -129,6 +129,7 @@ ReAct模式最早出现的Agent设计模式,目前也是应用最广泛的。 ## 猴版实现 ### ReAct + ```python class LLMSingleActionAgent { llm: AzureLLM diff --git a/_posts/MachineLearning/LLM/2024-06-10-multi_agent.md b/_posts/MachineLearning/LLM/2024-06-10-multi_agent.md index a4708776..64b4cc06 100644 --- a/_posts/MachineLearning/LLM/2024-06-10-multi_agent.md +++ b/_posts/MachineLearning/LLM/2024-06-10-multi_agent.md @@ -158,6 +158,10 @@ AutoGen允许在一个群聊中,调用另外一个Agent群聊来执行对话( [初识 OpenAI 的 Swarm:轻量级、多智能体系统的探索利器](https://mp.weixin.qq.com/s/XMMD_19g1CzDUzfeSm2qPQ) 未细读。 +## autogen-magentic-one + +https://github.com/microsoft/autogen/tree/main/python/packages/autogen-magentic-one + ## AutoGPT Andrej Karpathy 在 2017 年提出的 Software 2.0:基于神经网络的软件设计。真的很有前瞻性了。这进一步引出了当前正在迅速发展的 Agent Ecosystem。AutoGPT ,BabyAGI 和 HuggingGPT 这些项目形象生动地为我们展示了 LLM 的潜力除了在生成内容、故事、论文等方面,它还具有强大的通用问题解决能力。如果说 ChatGPT 本身的突破体现在人们意识到**语言可以成为一种服务**,成为人和机器之间最自然的沟通接口,这一轮新发展的关键在于人们意识到语言(不一定是自然语言,也包括命令、代码、错误信息)也是模型和自身、模型和模型以及模型和外部世界之间最自然的接口,让 AI agent 在思考和表达之外增加了调度、结果反馈和自我修正这些新的功能模块。于是**在人类用自然语言给 AI 定义任务目标(只有这一步在实质上需要人类参与)之后可以形成一个自动运行的循环**: diff --git a/_posts/MachineLearning/Model/2019-08-31-mathematical_interest.md b/_posts/MachineLearning/Model/2019-08-31-mathematical_interest.md index 662d1a4a..164f15af 100644 --- a/_posts/MachineLearning/Model/2019-08-31-mathematical_interest.md +++ b/_posts/MachineLearning/Model/2019-08-31-mathematical_interest.md @@ -222,9 +222,14 @@ $$g(z)=\frac{1}{1+e^{-z}}$$ ### 激活函数为什么使用Sigmoid +为了让输出能够平滑0到1的中间态,需要对结果进行连续性改造。 1. 它的输入范围是正负无穷,而值刚好为(0,1),正好满足概率分布为(0,1)的要求。我们**用概率去描述分类器**,自然比单纯的某个阈值要方便很多 2. 它是一个单调上升的函数,具有良好的连续性,不存在不连续点 +Sigmoid优点输出空间在(0, 1),缺点是左右两侧导数趋0,会出现梯度消失。 + +激活函数主要是将结果非线性化,放大参数对特征识别的灵敏和多样性,不同的激活函数除了非线性转换外,主要的区别是计算效率,解决梯度消失,中心值偏移这些问题。让训练能够有一定的“基因突变”。 + ### 损失函数为什么使用对数损失函数 $h_\theta(x)$与y 只有0和1两个取值 diff --git a/_posts/MachineLearning/Model/2021-10-31-from_rnn_to_attention.md b/_posts/MachineLearning/Model/2021-10-31-from_rnn_to_attention.md index 0e734717..b49a3cda 100644 --- a/_posts/MachineLearning/Model/2021-10-31-from_rnn_to_attention.md +++ b/_posts/MachineLearning/Model/2021-10-31-from_rnn_to_attention.md @@ -190,6 +190,10 @@ Seq2Seq模型可以认为是一个序列到序列转换的通用框架,具有 ## 注意力机制/抛弃RNN +CNN和RNN已经具备通过神经网络实现分类、预测能力,但是存在两个典型问题: +1. CNN聚焦局部信息,丢失全局信息。 +2. RNN无法并行计算(串行模式理论上确实也可以做到窗口无限大,然后从左到右把全部信息带过来,效率太差)。 + [从RNN到“只要注意力”——Transformer模型](https://zhuanlan.zhihu.com/p/353423931)基于RNN的架构存在着一个明显弊端,那就是RNN属于序列模型,需要以一个接一个的序列化方式进行信息处理,注意力权重需要等待序列全部输入模型之后才能确定,即需要RNN对序列“从头看到尾”。这种架构无论是在训练环节还是推断环节,都具有大量的时间开销,并且难以实现并行处理。例如面对翻译问题“A magazine is stuck in the gun.”,其中的“magazine”到底应该翻译为“杂志”还是“弹匣”?当看到“gun”一词时,将“magazine”翻译为“弹匣”才确认无疑。在基于RNN的机器翻译模型中,需要一步步的顺序处理从magazine到gun的所有词语,而当它们相距较远时RNN中存储的信息将不断被稀释,翻译效果常常难以尽人意,而且效率非常很低。**我们不禁要问一个问题:RNN 结构是否真的必要?**谷歌大脑、谷歌研究院等团队于 2017 年联合发表文章《Attention Is All You Need》,给出了的答案——“RNN is unnecessary, attention is all you need”。PS:为什么RNN上下文理解能力弱?因为RNN通过一个隐藏层记录当前及之前所见过的词汇,已经将语义信息杂糅在一起,而往往理解 it 这个词的语义时候,通过几个词就行,而不是it 之前的所有词汇。 [NLP注意力机制的视觉应用——谈谈看图说话的SAT模型](https://zhuanlan.zhihu.com/p/353350370) diff --git a/_posts/MachineLearning/Model/2023-10-30-from_attention_to_transformer.md b/_posts/MachineLearning/Model/2023-10-30-from_attention_to_transformer.md index faac3fc5..5bddd6c7 100644 --- a/_posts/MachineLearning/Model/2023-10-30-from_attention_to_transformer.md +++ b/_posts/MachineLearning/Model/2023-10-30-from_attention_to_transformer.md @@ -195,11 +195,12 @@ PS: head的概念类似卷积中的通道,只不过每个通道的输入都是 Attention模块的作用就是确定上下文中哪些词之间有语义关系,以及如何准确地理解这些含义(更新相应的向量)。这里说的“含义”meaning指的是编码在向量中的信息。Attention模块让输入向量们彼此充分交换了信息(例如machine learning model和fashion model,单词“model”指的应该是“模特”还是“模型”), 然后,这些向量会进入第三个处理阶段:Feed-forward / MLPs。针对所有向量做一次性变换。这个阶段,向量不再互相"交流",而是并行地经历同一处理。**Transformer基本不断重复Attention和Feed-forward这两个基本结构,这两个模块的组合成为神经网络的一层**。输入向量通过attention更新彼此;feed-forward 模块将这些更新之后的向量做统一变换,得到这一层的输出向量;在Attention模块和多层感知机(MLP)模块之间不断切换。 +FFN 设计的初衷,其实就是为模型引入非线性变换。 1. FFN的作用就是空间变换。FFN包含了2层linear transformation层,中间的激活函数是ReLu。 -2. attention层的output最后会和相乘,为什么这里又要增加一个2层的FFN网络?**Attention内部就是对特征向量V加权平均的过程。只用self-Attention搭建的网络结构就只有线性表达能力**。FFN的加入引入了非线性(ReLu激活函数),**变换了attention output的空间**, 从而增加了模型的表现能力。把FFN去掉模型也是可以用的,但是效果差了很多。 -$$ -FFN(x) = max(0,xW_1+b1)W_2+b_2 -$$ +2. attention层的output最后会和相乘,为什么这里又要增加一个2层的FFN网络?仔细看一下 Attention 的计算公式,其中确实有一个针对 q 和 k 的 softmax 的非线性运算。但是对于 value 来说,并没有任何的非线性变换。所以每一次 Attention 的计算相当于是对 value 代表的向量进行了加权平均,虽然权重是非线性的权重。**Attention内部就是对特征向量V加权平均的过程。只用self-Attention搭建的网络结构就只有线性表达能力**。FFN的加入引入了非线性(ReLu激活函数),**变换了attention output的空间**, 从而增加了模型的表现能力。把FFN去掉模型也是可以用的,但是效果差了很多。 + $$ + FFN(x) = max(0,xW_1+b1)W_2+b_2 + $$ 2. 前馈神经网络的输入是self-attention的输出,是一个矩阵(即上图Z),矩阵的维度是(序列长度×D词向量),**之后前馈神经网络的输出也是同样的维度**。self-attention + 前馈神经网络 就是一个小编码器的内部构造了,一个大的编码部分就是将这个过程重复了6次,最终得到整个编码部分的输出。为了解决梯度消失的问题,在Encoders和Decoder中都是用了残差神经网络的结构,即每一个前馈神经网络的输入不光包含上述self-attention的输出Z,还包含最原始的输入。 ```python @@ -223,14 +224,15 @@ class MLP(nn.Module): 其它 -1. 大语言模型中的参数都用在哪了?可以看到在百亿参数量以上,差不多三分之二的参数实际上是 FFN 参数,剩下的基本都是 attention 参数。所以虽然论文名叫 attention is all you need,但实际上 FFN 仍然起到了很重要的作用。 +1. 大语言模型中的参数都用在哪了?可以看到在百亿参数量以上,**差不多三分之二的参数实际上是 FFN 参数**,剩下的基本都是 attention 参数。所以虽然论文名叫 attention is all you need,但实际上 FFN 仍然起到了很重要的作用。 2. transformer 中最重要的是self-attention,self-attention 由三个线性矩阵Q、K、V 决定,如果我们把Q、K矩阵设置为零,那么self-attention 就变成了FFN,$Z_0 =V_0*X$,也就是说,FFN是self-attention 的一个特例,FFN能表达的逻辑,self-attention 也可以,但反过来却不成立。至于transformer 中的FFN部分,当初设计是为了输入输出维度的对齐,毕竟多注意力的输出 $W_0$的维度比输入X维度高很多。但如果一定要用FFN去表达self-attention的逻辑,也是可以的,但需要的参数量却要大很多,感兴趣的可以去试验一下,用FFN去拟合self-attention 的逻辑。就好像乘法能计算的东西,单纯用加法依然可以做到,但效率要低很多,self-attention就是乘法,FFN就是加法。 [大模型结构基础(四):前馈网络层的升级](https://zhuanlan.zhihu.com/p/702190813) 未读。FFN组件的一个显著进步是混合专家(MoE)架构,它采用稀疏激活的FFN。在MoE中,每个输入只有一部分FFN层(或专家)被激活,显著减少了计算负载,同时保持了高模型容量。 ### Layer Normalization/对应Norm + [Batch Normalization原理与实战](https://mp.weixin.qq.com/s/7B-gSLQm0PAKMefKHMb8nw) 是一种常规的模型“构件”,非transformer独有。 -归一化核心是为了让不同层输入的取值范围或者分布能够比较一致。由于深度神经网络中每一层的输入都是上一层的输出,因此多层传递下,对网络中较高的层,之前的所有神经层的参数变化会导致其输入的分布发生较大的改变。也就是说,随着神经网络参数的更新,各层的输出分布是不相同的,且差异会随着网络深度的增大而增大。但是,需要预测的条件分布始终是相同的,从而也就造成了预测的误差。因此,在深度神经网络中,往往需要归一化操作,将每一层的输入都归一化成标准正态分布。 +归一化核心是为了让不同层输入的取值范围或者分布能够比较一致。由于深度神经网络中每一层的输入都是上一层的输出,**因此多层传递下,对网络中较高的层,之前的所有神经层的参数变化会导致其输入的分布发生较大的改变**。也就是说,随着神经网络参数的更新,各层的输出分布是不相同的,且差异会随着网络深度的增大而增大。**但是,需要预测的条件分布始终是相同的**,从而也就造成了预测的误差。因此,在深度神经网络中,往往需要归一化操作,将每一层的输入都归一化成标准正态分布。 Normalization有两种方法,Batch Normalization和Layer Normalization。关于两者区别不再详述。 @@ -368,7 +370,7 @@ $$ 3. 平均每个token的计算量为 $C_{token}=\frac{C}{b s} = 72ld^2 + 12lsd = 6N (1+\frac{s}{6 d}) \approx 6N(s \le 6d)$ 4. 所以对于包含全部D个token的数据集 $C = C_{token}D \approx 6ND$ -PS:mlp参数量和计算量都不输attention。 $C_{token} \approx 6N$ 可以认为是正反向3倍*加法乘法2倍=6倍。 +PS:mlp参数量和计算量都不输attention。 $C_{token} \approx 6N$ 可以认为是正反向3倍*加法乘法2倍=6倍。 一般推演的时候,可以先只考虑一个句子的 输入10个token 输出10个token的变换,实际计算时,还要再考虑batch 维度。 ### 代码 diff --git a/_posts/MachineLearning/Model/2024-10-11-bert.md b/_posts/MachineLearning/Model/2024-10-11-bert.md index 6bf26ac8..28fddcb7 100644 --- a/_posts/MachineLearning/Model/2024-10-11-bert.md +++ b/_posts/MachineLearning/Model/2024-10-11-bert.md @@ -75,7 +75,7 @@ BERT的论文为我们介绍了几种BERT可以处理的NLP任务: 2. 文本分类 3. QA机器人 4. 语义标注 -5. 特征提取 ==> rag 里的emebedding +5. 特征提取 ==> rag 里的emebedding。 比如说我们自己训练一个类似BERT的模型,通过周围的词来预测完形填空试卷,“____是法国的首都”,通过一个模型训练词的上下文联系性之后,形成特定的词向量表。 针对不同任务,BERT 采用不同部分的输出做预测,分类任务利用[CLS]位置的 embedding,NER 任务利用每个 token 的输出 embedding。PS:最后一层的输出 选用[cls] 对应的embedding 或多个emebedding 套个FFNN + softmax,二分类或多分类任务就都可以解决了。**可以认为bert 对输入的文本做了一个编码**。 diff --git a/_posts/Technology/Algorithm/2016-03-15-data_structure.md b/_posts/Technology/Algorithm/2016-03-15-data_structure.md index 64f71281..4cdb5730 100644 --- a/_posts/Technology/Algorithm/2016-03-15-data_structure.md +++ b/_posts/Technology/Algorithm/2016-03-15-data_structure.md @@ -374,7 +374,7 @@ DFS 一般用栈实现,BFS 一般用队列实现,可以看看这些算法在 2. 从缓存中删除一个数据 3. 在缓存中查找一个数据 -这三个操作都涉及到查找操作,换句话说,**对数据的操作无外乎crud,而cud都离不开查询**。进而很明显,**基于有序结构的查询速度是最快的**,也就引出了排序算法的重要性。PS:查找 ==> 遍历(线性结构是遍历,树形图形结构是回溯) ==> 排序、动态规划、贪心等加速遍历(排序也可以作为预处理的一种手段)==> 分治 +这三个操作都涉及到查找操作,换句话说,**对数据的操作无外乎crud,而cud都离不开查询r**。进而很明显,**基于有序结构的查询速度是最快的**,也就引出了排序算法的重要性。PS:查找 ==> 遍历(线性结构是遍历,树形图形结构是回溯) ==> 排序、动态规划、贪心等加速遍历(排序也可以作为预处理的一种手段)==> 分治 2. 查找基本办法是遍历,不同的数据结构不同的遍历方法,迭代、递归(f(n)展开为f(n-1) 也是一种遍历)等等,**遍历即暴力搜索**,需要利用“搜索目标的特性”在遍历过程中剪枝。**一般要充分利用解空间的特性、规律 来加速遍历**,有序是规律的一种。 1. 针对不同的数据结构,查找的概念也会泛化。比如图中的查找最短路径,查找数组内符合某个条件的连续子序列(两个指针)/子序列/排列(可以认为是一个多叉树) 1. 遍历的元素类型有多种,遍历可以轮询数据结构的每个节点,也可以是轮询数据结构的每个可能的子结构(比如字符串、数组的子序列)。比如线性表的遍历元素、遍历子串,树的遍历元素、遍历所有路径、遍历所有根到叶子节点路径;一次遍历多个元素(比如双指针、快慢指针) @@ -394,6 +394,8 @@ DFS 一般用栈实现,BFS 一般用队列实现,可以看看这些算法在 3. 很多思路就是从暴力法来的,从笨方法优化而来,这是最稳妥的。暴力法不需要想思路,**是你解答所有问题的思维起点**,就是遍历所有可能,**判断其是否是解,而不是去拼凑一个解**。你直接想一个精巧的方法, 很可能不符合所有情况。很多思路都是优化中得到的思路,最后再冠以一个合理的解释,我们要是直接学了这些解释,很容易就碰到具体的问题又忘了。PS:这也是为什么《算法通关之路》不厌其烦的每道题先讲一下暴力法。 2. 暴力法之所以有问题,一方面是因为它产生重复的计算;另一方面暴力法往往还会去执行一些根本不可能是结果的代码。可以通过辅助数据结构、剪枝、滑动窗口来优化。注意,**你要砍掉的是不可能是结果的遍历,而不是想办法去拼凑解,因为你总是很难罗列所有情况**。PS:字节一面算法题的教训 +既然排序,就得有容器。排序排序,得有个容器,把东西放里面排排放,那才有顺序的概念。于是就有了 “容器” 的概念。为了操作容器,就要对容器进行更高程度的抽象,于是就有了 “迭代器”的概念。迭代器,就是用来访问容器里存储的对象的。是容器的抓手。 + ## 碎碎念 ### 查询的不同意涵 diff --git a/_posts/Technology/Architecture/2018-10-01-object_oriented.md b/_posts/Technology/Architecture/2018-10-01-object_oriented.md index 4a269352..1c1eb8a3 100644 --- a/_posts/Technology/Architecture/2018-10-01-object_oriented.md +++ b/_posts/Technology/Architecture/2018-10-01-object_oriented.md @@ -262,6 +262,19 @@ void silly_operation(struct file_operations* operations) { ## 其它 +面向对象三要素封装、继承、多态。 +1. 封装,封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项,或者叫接口。**有了封装,就可以明确区分内外**,使得类实现者可以修改封装内的东西而不影响外部调用者;而外部调用者也可以知道自己不可以碰哪里。这就提供一个良好的合作基础——或者说,只要接口这个基础约定不变,则代码改变不足为虑。 + 1. 这和带兵打仗是类似的,班长需要知道每个战士的姓名/性格/特长,否则就不知道该派谁去对付对面山坡上的狙击手;而连长呢,只需知道自己手下哪个班/排擅长什么就行了,然后安排他们各自去守一段战线;到了师长/军长那里,他更关注战场形势的转变及预期……没有这种层层简化、而是必须直接指挥到每个人的话,累死军长都没法指挥哪怕只是一场形势明朗的冲突——光一个个打完电话就能把他累成哑巴。反过来也对:军长压根就不应该去干涉某个步兵班里、几个大头兵之间的战术配合;这不仅耽误他行使身为军长的职责,也会干扰士兵们长久以来养成的默契。他的职责是让合适的部队在合适的时机出现在合适的战场,而不是一天到晚对着几个小兵指手画脚、弄的他们无所适从。约束各单位履行各自的职责、禁止它们越级胡乱指挥,这就是封装。 + 2. 什么是真正的封装?封装是不是等于“把不想让别人看到、以后可能修改的东西用private隐藏起来”?显然不是。如果功能得不到满足、或者未曾预料到真正发生的需求变更,那么你怎么把一个成员变量/函数放到private里面的,将来就必须怎么把它挪出来。真正的封装是,经过深入的思考,做出良好的抽象,给出“完整且最小”的接口,并使得内部细节可以对外透明。注意:对外透明的意思是,外部调用者可以顺利的得到自己想要的任何功能,完全意识不到内部细节的存在。一个典型的例子,就是C++的new和过于灵活的内存使用方式之间的耦合。这个耦合就导致了new[]/delete[]、placement new/placement delete之类怪异的东西:**这些东西必须成对使用,怎么分配就必须怎么释放,任何错误搭配都可能导致程序崩溃**——这是为了兼容C、以及得到更高执行效率的无奈之举;但,它更是“抽象层次过于复杂,以至于无法做出真正透明的设计”的典型案例。 +1. 继承同时具有两种含义:其一是继承基类的方法,并做出自己的改变和/或扩展——号称解决了代码重用问题;其二是声明某个子类兼容于某基类(或者说,接口上完全兼容于基类),外部调用者可无需关注其差别。实践中,继承的第一种含义(实现继承)意义并不很大,甚至常常是有害的。因为它使得子类与基类出现强耦合。继承的第二种含义非常重要。它又叫“接口继承”。接口继承实质上是要求“做出一个良好的抽象,这个抽象规定了一个兼容接口,使得外部调用者无需关心具体细节,可一视同仁的处理实现了特定接口的所有对象”——这在程序设计上,叫做归一化。 + 1. 没有真正的抓到一类事物(在当前应用场景下)的根本,就去设计继承结构,是必不会有所得的。不仅如此,请注意我强调了在当前应用场景下。这是因为,分类是一个极其主观的东西,不存在普适的分类法。举例来说,我要研究种族歧视,那么必然以肤色分类;换到法医学,那就按死因分类;生物学呢,则搞门科目属种…… + 2. 最具重量级的炸弹则是:正方形是不是一个矩形?它该不该从矩形继承?如果可以从矩形继承,那么什么是正方形的长和宽?在这个设计里,如果我修改了正方形的长,那么这个正方形类还能不能叫正方形?它不应该自然转换成长方形吗?如果我有两个List,一个存长方形,一个存正方形,自动转换后的对象能否自动迁移到合适的list?什么语言能提供这种机制?如果不能,“一视同仁的处理某个容器中的所有元素”岂不变成了一句屁话?造成这颗炸弹的根本原因是,**面向对象中的“类”,和我们日常语言乃至数学语言中的“类”根本就不是一码事**。面向对象中的“类”,意思是“接口上兼容的一系列对象”,关注的只不过是接口的兼容性而已(可搜索 里氏代换);关键放在“可一视同仁的处理”上(学术上叫is-a)。显然,这个定义完全是且只是为了应付归一化的需要。这个定义经常和我们日常对话中提到的类概念上重合;但,如前所述,根本上却彻彻底底是八杆子打不着的两码事。 +1. 多态实质上是继承的实现细节;那么让多态与封装、继承这两个概念并列,显然是不符合逻辑的。 +总结:面向对象的好处实际就这么两点。 +1. 一是通过封装明确定义了何谓接口、何谓接口内部实现、何谓接口的外部调用者,使得大家各司其职,不得越界; +2. 二是通过继承+多态这种内置机制,**在语言的层面支持归一化的设计**,并使得内行可以从代码本身看到这个设计。但,注意仅仅只是支持归一化的设计。不懂如何做出这种设计的外行仍然不可能从瞎胡闹的设计中得到任何好处。 + + ### 左耳听风 来自陈皓《左耳听风》付费课程,建议先看下[java 语言的动态性](http://qiankunli.github.io/2018/08/15/java_dynamic.html) diff --git a/_posts/Technology/Architecture/2019-08-14-complexity.md b/_posts/Technology/Architecture/2019-08-14-complexity.md index 9941b450..7b39ba53 100644 --- a/_posts/Technology/Architecture/2019-08-14-complexity.md +++ b/_posts/Technology/Architecture/2019-08-14-complexity.md @@ -181,7 +181,7 @@ OOP可以看作一种在程序中包含各种独立而又互相调用的对象 [你写的代码是别人的噩梦吗?从领域建模的必要性谈起](https://mp.weixin.qq.com/s/UHrJ-6ruC_HkhUXvWvDX0A) -李云:我们在编程时,其实包含无意识的两大步骤:**第一步完成的是根本任务,即构思好概念和概念之间的关系**,这一步我称之为“软件设计”;第二步完成的是次要任务,即在满足时间冗余度和空间冗余度的情形下,将概念用编程语言表达出来,这一步就是编码工作。布鲁克斯指出,过去软件生产效率的巨大进步得益于在次要任务上投入了巨大的努力。比如,新的编程语言、更快的处理器等。然而,除非次要任务占整个软件开发活动的 90%,否则即便将次要任务所花费的时间缩减到零,也不能带来软件生产效率数量级的提高。这就是“没有银弹”四个字的核心所指。即便进入 21 世纪的今天,这一论断依然成立。软件行业在根本任务的生产效率上,并没有取得次要任务那样质的进步。回到“程序易于理解的核心是什么”这个问题上,我对好程序的第二层理解是:**好程序有着清晰、颗粒度合适、连贯且一致的概念**。强调**概念质量**的背后,表达的是对程序的软件设计质量的高要求,这样的程序才易于理解。概念能力是指个体理解、分析和处理复杂概念和问题的能力。这种能力通常涉及从现象中抽象出关键信息,形成整合的观念和理论,进而能够对复杂的情境进行有效的理解和处理。你知道吗?概念能力是人类应对复杂度的一种独特能力,帮助我们理解并解决大量个例问题。新概念的提出一开始会带来更高的概念成本,需要我们花一定的时间去学习和掌握。可一旦概念被普及,就能提升沟通的效率。那我们为什么最终会喜欢新的概念呢?因为概念将大大降低大脑需要处理的信息量,降低生物能耗,是进化带给我们的一种生存能力使然。在工作中,你可能听到过“分而治之”这个词,讲的是对一个复杂的软件系统,用以大化小、拼积木的方法来实现。当你知道了程序中概念的核心作用后,就可以理解为,复杂的软件系统是通过概念的切分与塑造来实现的。PS:一个设计的概念质量如何,低level的抽象是看到一堆细节。 +李云:我们在编程时,其实包含无意识的两大步骤:**第一步完成的是根本任务,即构思好概念和概念之间的关系**,这一步我称之为“软件设计”;第二步完成的是次要任务,即在满足时间冗余度和空间冗余度的情形下,将概念用编程语言表达出来,这一步就是编码工作。布鲁克斯指出,过去软件生产效率的巨大进步得益于在次要任务上投入了巨大的努力。比如,新的编程语言、更快的处理器等。然而,除非次要任务占整个软件开发活动的 90%,否则即便将次要任务所花费的时间缩减到零,也不能带来软件生产效率数量级的提高。这就是“没有银弹”四个字的核心所指。即便进入 21 世纪的今天,这一论断依然成立。软件行业在根本任务的生产效率上,并没有取得次要任务那样质的进步。回到“程序易于理解的核心是什么”这个问题上,我对好程序的第二层理解是:**好程序有着清晰、颗粒度合适、连贯且一致的概念**。强调**概念质量**的背后,表达的是对程序的软件设计质量的高要求,这样的程序才易于理解。概念能力是指个体理解、分析和处理复杂概念和问题的能力。这种能力通常涉及从现象中抽象出关键信息,形成整合的观念和理论,进而能够对复杂的情境进行有效的理解和处理。你知道吗?概念能力是人类应对复杂度的一种独特能力,帮助我们理解并解决大量个例问题。新概念的提出一开始会带来更高的概念成本,需要我们花一定的时间去学习和掌握。可一旦概念被普及,就能提升沟通的效率。那我们为什么最终会喜欢新的概念呢?因为概念将大大降低大脑需要处理的信息量,降低生物能耗,是进化带给我们的一种生存能力使然。在工作中,你可能听到过“分而治之”这个词,讲的是对一个复杂的软件系统,用以大化小、拼积木的方法来实现。当你知道了程序中概念的核心作用后,就可以理解为,复杂的软件系统是通过概念的切分与塑造来实现的。PS:一个设计的概念质量如何,低level的抽象是看到一堆细节。面向概念编程。 1. 第一层理解是:让人易于理解的程序才是好程序。 2. 第二层理解是:好程序有着清晰、颗粒度合适、连贯且一致的概念。 3. 好程序的第三层理解:好程序是让人容易修改的。对于代码来说,我们怕的不是它不完善,而是改不动。 diff --git a/_posts/Technology/Architecture/2019-08-20-clean_architecture_note.md b/_posts/Technology/Architecture/2019-08-20-clean_architecture_note.md index dc5c3e51..21c25426 100644 --- a/_posts/Technology/Architecture/2019-08-20-clean_architecture_note.md +++ b/_posts/Technology/Architecture/2019-08-20-clean_architecture_note.md @@ -232,4 +232,6 @@ PS:**对于业务来说,核心是model 和service/Usecase,对下是基础 ## 其它 +在大多数产品研发过程中,通常遵循一种瀑布式协作模式,在这个过程中,业务人员首先了解需求,然后由产品经理进行收集和分析,最后传达给技术人员实施。这种模式的问题在于,**它使得上游角色更加面向未来,而下游的技术人员则更多地依赖历史经验,处于信息劣势的地位**。这种信息的不对等很容易导致上游角色在制定方向和目标时,较少考虑实施路径的可行性。因此,当研发的产品出现问题时,人们往往倾向于归咎于架构的不灵活或缺乏前瞻性。当技术架构团队竭尽全力弥补信息劣势,提出一个相对可靠的架构方案,并能够识别出对未来需求复用有影响的改造点时,他们通常会与业务人员、产品经理一样,主动地自我合理化地认为,这样的架构优化一定会影响项目的上线时间,因此倾向于先实施临时方案,而不是进行架构优化。然而,当项目上线后出现问题,技术架构团队再次主动排查并提出清理和解决历史架构负债的方案时,业务人员和产品经理往往会指责说:这就是一个技术架构问题。 + 瀑布模型/大型架构像恐龙一样消失了,前期设计够用、后期进行大量重构的思想如小巧玲珑的哺乳动物一样替代了它们,软件架构迎来了响应式设计的时代/大型架构时代让位给易碎型(Fragile Architecture)架构。把架构设计工作交给程序猿的问题就是,程序猿必须学会像架构师一样思考问题。**我们的每一项决策都必须为未来的变化敞开大门**。就像打台球一样,我们的每一杆击球都不只是为了要把球打进洞里,它也事关下一次击球时所在的位置。**让我们现在编写的代码不对未来的代码产生阻碍**是一项非常重要的技能,通常需要花费多年时间才能掌握。 \ No newline at end of file diff --git a/_posts/Technology/DDD/2017-12-25-ddd.md b/_posts/Technology/DDD/2017-12-25-ddd.md index 1053e351..3b9d3096 100644 --- a/_posts/Technology/DDD/2017-12-25-ddd.md +++ b/_posts/Technology/DDD/2017-12-25-ddd.md @@ -188,6 +188,24 @@ Evic Evans在《领域驱动设计》中将软件系统的设计分为2个部分 9. 对于这些领域对象,无论是创建,还是修改,我们都需要有一个地方把变更的结果保存下来,而承担这个职责的就是仓库(Repository)。你可以简单地把它理解成持久化操作(当然,在不同的项目中,具体的处理还是会有些差别的)。 10. 当我们把领域服务构建起来之后,核心的业务逻辑基本就成型了。但要做一个系统,肯定还会有一些杂七杂八的东西,比如,用户要修改一个订单,但首先要保证这个订单是他的。在 DDD 中,承载这些内容的就是应用服务。应用服务和领域服务之间最大的区别就在于,领域服务包含业务逻辑,而应用服务不包含。一些与业务逻辑无关的内容都会放到应用服务中完成,比如,监控、身份认证等等。 +[一文详谈领域驱动设计实践](https://mp.weixin.qq.com/s/brbK62nty2cWBmjitvfgPQ) +1. **实体从根本上不由其属性来定义,而是由连续性和唯一性来定义**。在领域模型中,需要通过一个唯一标识而不是其属性来区分,且在其生命周期中具有连续性的对象,我们将它定义为一个实体。在实际应用的过程中,实体往往是需要持久化到数据库的,因此大部分情况下,我们都以实体的唯一标识作为数据库中的主键(而不是数据库的逐渐作为实体的唯一标识)。 +2. 一个实体往往会关联另外一个实体,这种关联关系主要包含一对一、一对多、多对多这三种类型。在领域模型里,一对多,多对多的关联,往往会让代码复杂度急剧上升。解决这个问题有几种思路:规定一个遍历的方向;添加限定;消除不必要的关联。实体间的关联,在数据库中经常会通过关系表来表达,但在领域对象中,完全可以通过类的引用关系来表示,不需要将关系抽象为实体(除非这个关系有特殊的业务意义)。 +3. 当一个实体内的部分属性,我们发现它们具有较强的相关性,这些属性单独抽象成一个对象可以更好的描述事物,且这个对象并不具备唯一性,我们就将它归类为值对象。值对象具备以下特征:不需要唯一标识来代表其唯一性;**一些有关系的属性的聚合**;不变性(值对象可以复制,并在对象间传递)。 +4. 实体关联的极简设计能够帮助我们描述现实世界事物之间的关系,并且能在一定程度上限制关系的复杂度增长,但随着业务发展,实体间的关系会越来越复杂,我们依然需要将这种关系表达在模型里,但是如果还是将这种关联表达在实体中,实体就会因各种关系带来的复杂性而膨胀,开发者也无法关注到模型的核心。当多个实体之间在某些场景下需要保持更改的一致性时,除了使用对象关联外,还可以建立一个**对象组**,将有着紧密关系的实体和值对象封装在一起,这个对象组就是领域模型中的聚合。聚合拥有两个重要特征: + 1. 边界:定义聚合内有什么,与其他聚合区分。 + 2. 聚合根:聚合中的一个特定实体 + 1. 选择聚合中的一个Entity作为聚合根; + 2. 通过根来控制对边界内其他对象的访问; + 3. 只允许外部对象保持对根的访问; + 4. 对边界内的其他对象通过根来遍历关联来发现; + 3. 在实际将聚合在代码中落地的过程中,有两种不同的写法: + 1. 一个对象,即是实体,也是聚合,同时是该聚合中的聚合根。此时,实体和聚合的概念经常容易搅在一起,只需要关注实体本身时,又不得不去考虑这个对象中关联的其他实体。 + 2. 在实体之上单独定义一个聚合对象(xxAggreagte),在其中选择一个实体作为聚合根。 + 5. 软件工程没有“银弹”,模型需要在实践中不断的演进和迭代,从简单到复杂,只要我们时刻关注模型是否能够反映业务实际情况。 +6. **查询不是领域模型**。不要因为对数据的查询需求而改变领域模型,领域模型是为了映射业务活动,以及业务活动的影响,这个影响可能是领域内的数据,也可能是对领域外的改变。在我们的开发过程中,页面的展示,对外提供查询接口往往是高频变更的地方,查询的逻辑也经常是无花八门,很难控制用户想要把哪些数据聚合在一起展示。因此对于这种纯查询的场景,我们不要用领域模型去承载,最简单直接的方式就是直接从数据层去查询、拼装数据。这也是命令查询的责任分离(Command Query Responsibility Segregation,CQRS)这种设计模式一种体现。 + 1. 以上所说的查询,和我们在写链路里需要从数据库中重建领域对象,是两种不同的场景。重建领域对象一般是通过repository来提供查询接口,返回的结果一定是领域对象,重建出来的领域对象也一定是在写入链路使用的。 +7. 工厂(Factory)。不同于设计模式中的工厂模式,这里的Factory仅仅是为了将领域对象创建的过程通过一种单独的模式独立出来。我们的一个系统,可能会对外提供多种类型、多种模式的入口,比如消息监听、端面、接口、定时任务等,不同的入口我们对外的契约不同,用户能提供的入参也不相同。我们使用领域驱动设计来作为代码设计的基本诉求是所有的核心业务代码都基于领域对象,**因此领域对象的创建是一切业务代码的开始**。Factory是承载将系统对外提供的请求模型转换为领域模型功能的一系列对象。 [深入理解领域驱动设计中的聚合](https://mp.weixin.qq.com/s/a5NiKLFZsg54P_fcXPkahg) 聚合的本质就是建立了一个比对象粒度更大的边界,聚集那些紧密关联的对象,形成了一个业务上的对象整体。使用聚合根作为对外的交互入口,从而保证了多个互相关联的对象的一致性。通过把对象组织为聚合,在基本的对象层次之上构造了一层新的封装。封装简化了概念,隐藏了细节,在外部需要关心的模型元素数量进一步减少,复杂性下降。但是不是所有相关对象都聚合到一块呢?聚合划分的原则 @@ -196,7 +214,6 @@ Evic Evans在《领域驱动设计》中将软件系统的设计分为2个部分 3. 场景频率一致性 4. 聚合内的元素尽可能少 - ### 读写分离 DDD读写对待不一样的,写需要严格遵守分层结构。读不一定,看情况。 diff --git a/_posts/Technology/DDD/2018-11-13-crud_defect.md b/_posts/Technology/DDD/2018-11-13-crud_defect.md index 9d30a167..07a69212 100644 --- a/_posts/Technology/DDD/2018-11-13-crud_defect.md +++ b/_posts/Technology/DDD/2018-11-13-crud_defect.md @@ -37,7 +37,16 @@ keywords: ddd cqrs [领域驱动设计在互联网业务开发中的实践](https://tech.meituan.com/DDD_in_%20practice.html)**在我们习惯了J2EE的开发模式后,Action/Service/DAO这种分层模式,会很自然地写出过程式代码**,而学到的很多关于OO理论的也毫无用武之地。使用这种开发方式,对象只是数据的载体,没有行为。以数据为中心,**以数据库ER设计作驱动。** PS,对这句深有体会,此时一个系统最有含量的部分就是数据库设计,数据库表定了,剩下的就是腾挪数据了。 -[阿里盒马领域驱动设计实践](http://www.infoq.com/cn/articles/alibaba-freshhema-ddd-practice) 形容这类代码“面条代码”,从(客户)端上一条线杀到数据库完成一个操作,仅有的一些设计集中在数据库上。 +[阿里盒马领域驱动设计实践](http://www.infoq.com/cn/articles/alibaba-freshhema-ddd-practice) 形容这类代码“面条代码”,从(客户)端上一条线杀到数据库完成一个操作,仅有的一些设计集中在数据库上。[一文详谈领域驱动设计实践](https://mp.weixin.qq.com/s/brbK62nty2cWBmjitvfgPQ) +1. 在公司看到过的大部分代码中的对象只有两种类型:服务类(Service Object)和数据类(Data Object),所有的数据对象,被无脑的开放了所有的Getter和Setter方法,加之lombok等语法糖的推波助澜,对象的封装变得更加困难了。而所有的业务逻辑都被堆在了各种Service中,当然好的团队依然会对这些Service类做很好的分层设计,使代码的演进还能够正常的进行。 + 1. 我们大部分应用使用的ORM框架,基本上都是用Mybatis,因此我们往往都需要有一个对象来映射数据库表结构,这里我将它命名为数据库对象,我们在代码中一般会通过DO、BO等后缀来进行区分。**也正因为这个原因,我们很多时候都会直接将数据库模型作为代码设计的目标**,代码逻辑也是为了操作数据库对象来写,导致代码中缺失真实业务场景的还原。 +2. 实际上我并不是要说这种开发方式不好,相反它能够在程序员中被广泛认可,其优势不言而喻,它能够让一个只要掌握编程语言的新手,快速的承接需求并交付,无需在代码设计和怎么写的问题上花费更多的精力和学习成本。大部分情况下,团队内的架构师只需要做好接口设计和数据库的设计,这个需求就可以完全交给一个新人去实现了。我把这种方式看作是一种**通过确定【输入】和【输出】来控制软件开发确定性的方式**。输入即程序对外提供的可以执行程序的入口,我们常见的像RPC接口、HTTP接口、消息监听、定时任务等。输出是程序对外部环境或者状态的影响,可以是数据库的写入、消息的广播推送、外部系统的调用等等。 +3. **在一个系统刚开始的阶段,这种方式能够以非常高的效率完成交付,这个阶段业务的本质复杂性低,技术的复杂性也低,程序的输入和输出链路比较单一**。更重要的是在人的方面,每个人都能够很好的理解这种开发方式,只要从输入到输出的转换没有问题,程序员们不会去关注其中潜在的设计问题,无论是新人还是老手,开发这样的软件都能得心应手。相比于使用领域驱动设计的思维进行开发,面向过程的这种开发方式更简单直接,对人和团队的要求更低,在人员变动频繁的现状中,它能带来更快速的交付。 +4. 然而随着系统逐渐的演进,业务的核心复杂性变高,系统之间的联系逐渐变多,面向过程的这种开发方式就显得捉襟见肘了。不知道大家能否在自己团队中找到这样的代码:上千行的方法;上百个属性的类;循环依赖的Service;无法控制的数据一致性;越来越多的分支逻辑...。这些问题本质上并不是我们采用哪种开发方式就能解决的,但它们一定能说明我们当前的代码设计是存在问题的,这就像埋在我们系统中的一个个定时炸弹,如果你足够小心,团队的质量保障足够充足,这颗炸弹在你工作的期间可能并不会引爆,但根据墨菲定律,它们早晚是会引爆的。潜在的风险是一方面,另一方面是我们的交付速度,理解成本,沟通成本,知识的传递,都会因为这些混乱的代码而变得缓慢和困难。应对软件复杂度的方法有很多,即使是使用面向过程的开发方式,也有很多设计模式和方法论能够去解决这些问题。 +5. 在进行领域驱动设计落地的过程中,我感觉到最大的一个困难点是面向对象思维的转变,**领域驱动设计实际上是基于面向对象设计的一种更高维度的设计模式**,但我们之中大部分的开发者,已经习惯于按照面向过程的方式来进行开发,即使我们在很多场合都在强调我们在使用面向对象,但实际上却事与愿违。经验越丰富,越资深的工程师,越无法跳出之前长期积累的认知,这种先入为主的思维定式改变起来尤为困难。还有源源不断的新人逐渐开始进入这个行业,成为一个软件工程师,他们被要求能够尽快的开始交付和产出,他们也只能去模仿团队现在的代码,逐渐熟练以后,也只会把这种开发方式奉为圣经,再次传承下去。之前提到,**我们现在的开发现状是通过【输入】和【输出】来进行设计,而领域驱动设计则是在其基础上增加了一层:【领域模型】。即所有的输入都要转换为领域模型,所有的输出也都要通过领域模型去完成。领域驱动设计的所有模块、模式、方法都是基于领域对象,为领域对象服务的**。领域模型本身作为对现实世界中我们所解决问题空间的抽象,它的演进与问题空间的演进原则上是一致的,之所以使用面向对象来作为领域模型的承载,主要原因还是面向对象更加符合当下人们对现实世界的认知,理解和使用都更加简单。**现实世界中大部分的“系统”,都是可以用对象,以及对象之间的关系来描述,认识、理解**。描述现实世界中的客观事物是人类哲学最早开始思考的问题,先秦时期的名家,古希腊的形而上学,都是基于此目的建立的。今天我们的工作又何尝不是在混乱复杂的世界中,寻找规律,将其通过有限的模型表达出来,再转换为机器可以理解的语言,形成软件或者系统,简化人与人,人与物,物与物之间的交互过程。 +6. 一定要将领域模型和数据库模型分离开,这样我们的业务代码仅需要关注领域模型,到需要持久化的时候再去关心如何将领域模型转换为数据库模型。如此,即使之后数据库的选型发生变化,对代码的改动也仅限于对象转换的那部分逻辑;**领域模型的迭代演进也可以更加自由,不受数据库设计的约束**。领域模型到数据库模型转换的过程中需要注意几个细节: + 1. 不要将数据库关注的属性,无脑添加到领域对象中去,比如id、gmt_created、gmt_modified等。 + 2. 实体间的关联,在数据库中经常会通过关系表来表达,但在领域对象中,完全可以通过类的引用关系来表示,不需要将关系抽象为实体(除非这个关系有特殊的业务意义)。 [领域驱动设计详解:是什么、为什么、怎么做?](https://mp.weixin.qq.com/s/jo3jNikkxrM4ouzvjA-fFQ)分层并没有问题,但是这种分层架构采用的是包的形式进行的层与层的隔离,**需要每一位开发同学理解并且自觉遵守以上规范**,但是在实际工作中我们发现很多同学对Service层和Manager层的区别并不是特别的清楚,即使清楚的同学大部分也并没有完全遵守手册中的规范。在实际的业务代码中Service层充斥了大量的第三方依赖,对系统的稳定性有很大的影响。每依赖一个第三方服务都要各种异常处理,这些异常处理的代码往往会和业务代码混在一起,当这种代码多了以后会使代码的可读性非常差。 diff --git a/_posts/Technology/DDD/2023-07-06-ddd_practice.md b/_posts/Technology/DDD/2023-07-06-ddd_practice.md index 0e70ad04..1b58a427 100644 --- a/_posts/Technology/DDD/2023-07-06-ddd_practice.md +++ b/_posts/Technology/DDD/2023-07-06-ddd_practice.md @@ -13,6 +13,8 @@ keywords: ddd cqrs * TOC {:toc} +如果我们不做工程,只是简单的写一个程序,我们都可以很熟练的使用面向对象,但就是因为工程的复杂性,导致我们没有办法随心所欲去用面向对象里的各种优秀设计。 + ## 理念 [How To Implement Domain-Driven Design (DDD) in Golang](https://programmingpercy.tech/blog/how-to-domain-driven-design-ddd-golang/) @@ -32,6 +34,11 @@ Domain-Driven Design is a way of structuring and modeling the software after the ## 从分层开始理解DDD +[一文详谈领域驱动设计实践](https://mp.weixin.qq.com/s/brbK62nty2cWBmjitvfgPQ)我们工作的代码库中最多类名后缀就是Service了,我们也应该被各种Service的调用层级、循环依赖教训过很多遍了,出现这种问题实际上还是我们对代码缺少设计,一股脑的把业务逻辑、系统逻辑、应用逻辑、基础设施等等随意组装到一起使用,本来每一个部分的复杂度就已经非常高了,我们还要将这些复杂度揉到一起。领域驱动设计给我们提供了一种分层治理的思路,将系统内的服务类分为几个大类:应用层服务、领域层服务、基础设施层服务。 +1. 应用层服务用于处理输入输出、与领域模型和领域服务之间的调度、连接基础设施层服务。 + 1. 在将领域模型与工程结合的过程中,应用服务(ApplicationService)扮演了十分重要的角色,它对入口、领域模型、外部依赖、基础设施等部分进行编排和调度,最终使领域模型能够在实际应用中正常工作。 +2. 当领域模型中某个动作或者操作不能看作某个领域对象自身的职责时,可以将其托管到一个单独的服务类中,这种服务类,我们把它叫做领域服务。对于领域服务的使用,经常很难去定义哪些行为或逻辑是应该托管到服务类中还是由领域对象自己来负责。全部托管到领域服务中,领域对象则会变成贫血模型,如果不托管,又容易因职责过多而导致领域对象过于膨胀。对于这个问题我们也没有太好的解决办法,软件工程的问题永远都是在Balance的过程中,当代码复杂度可控的范围内,我们尽量减少对领域服务的使用,如果领域对象开始出现膨胀的现象,那就将其托管到领域服务中。**对于领域服务,一定要把守住一条底线,领域服务一定不要有状态**,也就是我们所说的“纯函数”,这样做能够让领域服务保持单纯,仅关注于领域对象之间的关系和其状态的变化,而不会引入领域逻辑以外的复杂性。 + https://github.com/KendoCross/kendoDDD 概念太多了,换一个角度,从MVC/MVP/MVVM的分层架构入手,类比理解DDD经典的四层。然后融合自己已有的编码习惯和认知,按照各层的主要功能定位,可以写的代码范围约束,慢慢再结合理解DDD的概念完善代码编写。 分层,分层架构有一个重要的原则:每层只能与位于其下方的层发生耦合。 diff --git a/_posts/Technology/Python/2024-05-19-python_vm.md b/_posts/Technology/Python/2024-05-19-python_vm.md index b40b0a19..9f7b45f8 100644 --- a/_posts/Technology/Python/2024-05-19-python_vm.md +++ b/_posts/Technology/Python/2024-05-19-python_vm.md @@ -836,8 +836,20 @@ HiObject* Interpreter::eval_generator(Generator* g) { ``` 对于 generator 每次进来都不用新建一个 frame 对象,而是从 generator 里去获取。执行结束以后,也不用销毁这个 frame,这样局部变量就保存在这个 frame 中了。下一次迭代的时候,也就是 next 方法被调用的时候,就可以继续使用同一个 frame。这个 frame 的特殊的地方是它有两种类型的出口,一种是执行 yield 语句,另一种是 return 或者遇到异常。这两种出口的区别是,yield 语句退出时,不会销毁 frame,另一种就像其他普通函数一样,需要销毁这个 frame。PS:执行Generator.next 使用的是 Generator自己的frame,执行结束也不销毁。 + +## 异常处理 +[异常是怎么实现的?虚拟机是如何将异常抛出去的?](https://mp.weixin.qq.com/s/afGVB0JvoKkOwLvDt7pHCg)当 Python 程序中使用raise关键字引发异常时,Python 虚拟机按如下流程处理: +1. 异常设置与初步处理。当执行到raise语句或程序运行过程中出现错误触发异常时,虚拟机确定异常类型并设置相关错误信息。如除法运算中除数为 0,会通过相应函数(如PyErr_SetString)设置特定类型的异常(如ZeroDivisionError)及错误信息,并返回NULL。 +2. 跳转到错误处理标签。由于返回NULL,根据字节码指令执行逻辑,虚拟机跳转到对应的错误处理标签(如pop_2_error等),这些标签会进行栈清理操作(弹出相应数量栈元素),之后进入error标签。 +3. error标签中的关键操作。在error标签内,若栈帧不是入口栈帧且是完整的,会调用PyTraceBack_Here函数。此函数先获取当前异常对象,获取其已有的 traceback(可能为空),接着以当前栈帧为参数创建新的 traceback 对象,将新对象与已有 traceback 通过tb_next关联起来,然后将新的 traceback 对象设置为当前异常的 traceback 并重新设置异常。也就是异常信息的更新和传递依赖于线程状态对象。 +4. 异常传播与栈帧回退。创建 traceback 对象后,虚拟机会进入exception_unwind标签(此处假设未找到捕获逻辑),进而到达exit_unwind标签。在exit_unwind标签中,将当前线程状态对象中的活动栈帧设置为上一个栈帧,完成栈帧回退动作。此时,异常沿着栈帧链向上传播,若上一个栈帧中的函数因异常返回NULL,则重复上述从error标签开始的过程,不断更新 traceback 链表,继续寻找异常捕获逻辑。 + 1. 异常表(Exception table 由 PyCodeObject 对象的 co_exceptiontable 字段负责维护)记录的代码块范围和异常类型信息,指导虚拟机决定是在当前栈帧继续查找其他try-except结构,还是沿着栈帧链向上继续传播异常,直到找到合适的异常处理代码块或者到达最顶层栈帧 +5. 最终处理(未捕获异常)。如果异常一直传播到最顶层(如模块对应的栈帧)都未被捕获,虚拟机从线程状态对象中取出维护的 traceback 链表,遍历并输出其中信息到stderr中,展示详细异常信息(包含函数调用栈追溯、异常类型和错误信息等),然后解释器结束运行。 + ## 参考 +[Python 函数在底层长什么样子?](https://mp.weixin.qq.com/s/ZEax7w4-q8GbesdVJAG5Vg)Python 一切皆对象,函数也不例外,函数这种抽象机制在底层是通过 PyFunctionObject 结构体实现的。PS:python代码很多动作都会对应一个c++对象, `def func` 就是new 一个PyFunctionObject,class 就是new 一个kclass/object 等? + [Python虚拟机原理](https://time.geekbang.org/column/article/311823) ![](/public/upload/python/python_vm_geek.jpg) \ No newline at end of file diff --git a/_posts/Technology/Storage/2024-10-12-neo4j.md b/_posts/Technology/Storage/2024-10-12-neo4j.md index 3ef03c46..d3c78cd3 100644 --- a/_posts/Technology/Storage/2024-10-12-neo4j.md +++ b/_posts/Technology/Storage/2024-10-12-neo4j.md @@ -58,7 +58,7 @@ Cypher语言主要分为增删改查(CRUD)四个部分,也可抽象成读和 2. 更新属性使用SET,删除属性用REMOVE 3. 删除节点或关系使用DELETE 3. 通用: RETURN, ORDER BY , LIMIT , SKIP, WITH, UNWIND, UNION , CALL - 1. WITH将分段的查询连接在一起,传递给另外一部分作为查询的开始。WITH 会影响查询结果集里的变量,WITH 语句外的变量不会传递到后续查询中。PS: match 后跟的是模式,不适合再跟count 等等计算了,所以用with 帮了一手。 + 1. WITH将分段的查询连接在一起,传递给另外一部分作为查询的开始。WITH 会影响查询结果集里的变量,WITH 语句外的变量不会传递到后续查询中。PS: match 后跟的是模式,不适合再跟count 等等计算了,所以用with 帮了一手。如果没有WITH子句,每个查询部分(或子句)将独立执行,不会保留前一个部分的结果或变量。 2. UNION。将多个查询组合起来。和SQL类似,多个查询的列的名称和数量要一致! [一看就会的 Neo4j Cypher 语法](https://bytedance.larkoffice.com/docx/PsMsdLbyioKIA8xW3XJcijronOx) @@ -158,6 +158,11 @@ MATCH (n {name: $name}) RETURN n; ``` +其它 + +1. 基于属性过滤做一些操作,比如`MATCH (n) where n.id = xx DETACH DETETE n` 或者 `MATCH (n {id: xx}) DETACH DETETE n`都可以,但大佬们说后者性能更好,且有时只有后者生效。 + + ### 逻辑架构 ![](/public/upload/storage/neo4j_arch.jpg)