- 一个软件架构师,热爱阅读,观察,思考和行动
- A software architect who enjoys reading, observing, thinking and doing.
llama2.c 源码阅读
1. 概述 前OpenAI著名工程师Andrej Kapathy开源了llama2.c项目,该项目是llama2模型推理代码的C语言实现,用大概970行C代码实现了LLama2模型的推理算法。整个项目代码简洁高效,值得深度阅读。对掌握大模型推理算法的细节有极大的帮助。 2. 源码阅读 2.1 基础算法 RMS归一化公式是: $$ o_i = w_i \times x_i \times \frac {1}{\sqrt{\frac{1}{n}\sum_{j=0}^{n-1} x_j^2 + \epsilon}} $$ 其中,\(\epsilon\) 为防止分母为0的数值。还有RMS因子是对x的归一化,w变量是gain变量,重新缩放标准化后的输入向量。 // ---------------------------------------------------------------------------- // neural net blocks; the dynamics of the Transformer void rmsnorm(float* o, float* x, float* weight, int size) { // calculate sum of squares float ss = 0.0f; for (int j = 0; j < size; j++) { ss += x[j] * x[j]; } ss /= size; ss += 1e-5f; ss = 1....
X下云
背景 2022 年 10 月 27 日马斯克以 440 亿美元完成 Twitter私有化交易并改名X.com,整整一年过去后,X的工程架构也发生巨大的变化,今年10月27日X工程团队发表了一个推文,总结一年的巨大变化,我觉得这个帖子很有代表性,给所有做engineering的同学新的启发。 原文 https://twitter.com/XEng/status/1717754398410240018 This has been a year full of engineering excellence that sometimes can go unnoticed. Besides all the visible changes you see on our app, here are some of the most important improvements we have made under the hood. Consolidated the tech stacks for For you, Following, Search, Profiles, Lists, Communities and Explore around a singular product framework. Completely rebuilt the For you serving and ranking systems from the ground up, resulting in a decrease 90% reduction in lines of code from 700K to 70K, a 50% decrease in our compute footprint, and an 80% increase in the throughput of posts scored per request....
大语言模型如何扩充上下文长度?
我们在应用大语言模型遇到的最典型的限制就是输入文本的上下文长度。开源的模型的上下文长度限制从2K到32K不等,商业模型最大上下文限制从2K到100K范围。上下文长度对应用大语言模型有着非常关键的影响,包括知识增强、记忆等Agents完成的工作,都是为了解决大语言模型上下文长度限制而设计的。大语言模型为什么会有上下文长度限制?是否有方法能扩充长度到几倍甚至十几倍?这几个问题困扰我很久。最近一段时间经过调研之后,我发现这些问题已经有了令人兴奋的进展,我也收获一些心得,记录于此。 先说结论: LLM的训练和计算都是没有上下文长度限制的,限制的只有计算资源和模型效果 头部公司和开源社区都有了阶段性的成果,最新的transformers,llama.cpp等开源项目已经内置了扩充上下文长度方法 如何在扩充上下文长度的同时,降低训练成本,保证模型效果,是一个还在不断探索的话题 LLM的上下文长度限制之谜 实际上,目前以Transformer为核心的LLM,理论上而言是没有上下文长度限制的。唯一限制上下文长度的,只有训练时的资源消耗,以及预测时的输出效果。如果不考虑这两点,LLM完全可以支持任意长度的上下文,这里本质原因是,Transformer的核心:Attention算法和上下文长度无关。 为说明这个本质原因,我们回顾下Attention的计算,参考「Attention is all your need」经典论文1 : 定义n为输入的token数量,vocab_size为token词典大小,d为文本embedding的维度大小,k为Query和Key向量的维度大小。那么整个Attention计算过程如下: Inputs是n个token序列,查找(vocab_size, d)大小的Embedding词典,转化为(n, d)的输入矩阵X 类似的,将输入的token位置i经过位置向量计算(查表或者实时计算),转化为(n, d)的词典,和上面的X词典相加,获得带上位置向量的X作为输入。注意位置向量的计算有两种方法,一种是通过查表的方式,即查找一个(pos_size, d)大小的Embedding词典,另外一种是实时计算,根据token的位置i,通过位置embedding算法,计算出对应的位置向量。这两种方法各有优缺点,这将是突破上下文长度限制的重点。 将X乘以\(W^Q,W^K和W^V\)三个Q,K,V权重矩阵,获得Q,K,V值矩阵。其中\(W^Q\)形状为(d, k), \(W^K\)形状为(d,k), \(W^V\)形状为(d, v),注意着三个权重矩阵都和输入长度无关,获得的Q和K矩阵大小是(n, k),V矩阵大小是(n, v) 如下计算attention: $$Attention(Q,K,V) =softmax(\frac {QK^T}{\sqrt{k}})V$$ 其中\(QK^T\)计算结果为(n, n)矩阵,再乘以V的,输出结果为(n,v)矩阵。注意这些计算都是实时计算,计算复杂度和输入长度有关。 5. 在Multi-Head Attention算法中,上述4个步骤所有矩阵变成了张量,增加了h个header,输入矩阵X变成(h, n, d)大小,\(W_q\)大小为(h, d, k), \(W_k\)大小为(h, d, k), \(W_v\)大小为(h, d, v)。Q, K, V矩阵分别大小为(h, n, k), (h, n, k), (h, n, v)。通过将多头concat,输出(n, hv)大小的结果\(Attention(Q,K,V)\),再经过一次线性变化,最终Attention结果为: $$MultiHead(Q, K, V) = Concat(Attention(Q, K, V))W^O$$ \(W^O\)大小为(hv, d),所以最终Attention结果\(MultiHead\)维度为(n, d)。 从上面的计算可见,整个模型的参数,只有位置向量需要查表的时候,是和上下文长度有关,而其他的所有权重矩阵,都和上下文长度无关。如果位置向量实时计算时,attention算法所依赖的所有参数都和上下文长度无关。 那么限制到底在哪里呢?上面已经提到,限制就在: 计算资源 模型效果 先说计算资源的限制,可以证明(过程略),上述第三步计算Q,K,V矩阵的计算复杂度是\(O(nd^2)\),第四步计算attention的计算复杂度是\(O(n^2d)\),所以计算attention总体的计算复杂度是\(O(nd^2+n^2d)\),如果d > n,则\(O(nd^2)\)占计算复杂度的大头,例如LLaMa1模型的n为2048,d为4096,所以可以估计训练复杂度和训练输入的上下文长度呈线性关系。以LLaMa1模型举例,训练一次的计算成本约为300万美元,如果将输入长度扩大到8倍(即16K),那么训练成本将增长到2400万美元。因此如果要在预训练阶段用更长的上下文长度训练,这个成本将变得难以接受。...
大模型时代,我们如何重构产品
近期厂内关于AI云原生思维的讨论非常热烈,尤其是AI大模型时代,产品如何重做重构的话题异常火热。关于这个话题,我倒是联想到一句经典的谚语:“It is like teenage sex: Everyone talks about it, nobody really knows how to do it, everyone thinks everyone else is doing it, so everyone claims”. 但说说容易,我们应该如何思考这个话题呢?在我看来,需要引入一些模型化思维来拆解这个问题,这样很多现象就能得到很好的解释,关于如何重构产品也就能逐步看到清晰的路线了。 首先,我们可以观察到,一切复杂系统,包括人体,组织,社会,甚至互联网,都是由三个子系统组成: 信息系统:数据,文章,网页,图片,视频等等一切我们能看,能读,能听,能感受到的东西,这个大千世界充满了信息,信息可以是数字化的,例如互联网上的数据,也可以是模拟的,例如线下的交流对话等 模型系统:通过各种推理,拆解,演绎,总结等手段,将信息转化为知识沉淀下来,例如人的大脑内部就存在一个抽象的世界模型,这个模型赋予了我们认知的能力,让我们可以产生意识,理解这个世界 行动系统:当模型处理完信息之后,它需要执行一定的操作和这个环境互动,这就是行动系统的作用。例如我开车过程中看到了一只小狗,我的大脑获取这个信息后,交给认知模型进行分析和处理,认知模型决策需要避开,我的行动系统的手和脚操作方向盘和刹车,完美避开相撞的风险。 当把互联网这样的复杂系统拆解为这三个子系统之后,最近三十年互联网发生的事情,就能用这三个系统的变化得到如下分类: 信息时代:从1995年开始,我们进入了信息时代。这个时代的互联网产品,本质上都是在做信息的搬运工。这其中还细分多个阶段,例如90年代开始由论坛,新闻门户网站组成的Web1.0阶段,初步形成信息的数字化。典型的代表有雅虎,新浪,搜狐和网易这些老牌门户网站。从2000开始,随着Web2.0的兴起,我们有了更高效的搜索引擎,社交网络,电子商务平台,实现了信息更高效的运转,真正将获取信息的边际成本降为了0。这个阶段出现了很多伟大的公司,典型的代表有Google,Amazon,百度,腾讯,阿里等。正是有了这些平台,我们才真正实现了获取信息效率的极大提升。到了2010年以后逐步兴起的移动互联网,又诞生了一批新的平台公司,实现了移动设备上的信息生产和消费,让用户可以随时随地地获取信息,使用信息 模型时代:其实从2010年前后,互联网平台产品就已经开始具备了模型能力,但这种模型还是“小”模型,并不具备转化为知识的能力。例如在搜索引擎中,我们有LTR模型来优化排序,在推荐引擎中也有稀疏大模型来优化推荐的列表。但这些模型都专属于某一个垂直领域,无法通用化使用。一个搜索引擎的模型是无法用在推荐系统中的,模型应用的边际成本依然很高。直到2022年开始,以OpenAI的ChatGPT为代表的大模型真正登台之后,我们才真正步入了模型极大发展的阶段。代表特色就是这种模型具备通用的认知能力,将使用和获取知识的边际成本真正降为0。举个例子来说,在模型时代之前,我们想了解一种疾病如何治疗,需要去阅读,去理解各种专业知识,甚至花费数年专业的学习才能真正解决临床上的病症。但如果有一个经过医学专业知识训练好的大模型,通过大模型就直接获得了治疗方法,这就是将获取知识的成本降为了0。当前我们正处于这个模型时代刚开始的时期 下一个时代——行动时代:行动时代其实已经初露雏形了。典型的代表就是自动驾驶。自动驾驶是将我们驾驶行动的边际成本降为0,我们不需要付出太多获取信息-认知处理-操作汽车这一系列的成本,自动驾驶就自动替我们解决了。这个时代同样也处于早期阶段,但并不遥远 因此,在当前的模型时代,我们需要思考的是,如何用大模型来改造以及创造各种产品,将各行各业需要人来认知的过程的边际成本降为0,将是这个时代的主旋律。 以New Bing为例,为什么搜索是第一个大模型为先的应用及平台,从上面的分析可以获取答案。在信息时代,搜索产品本质上是人通过搜索query来找信息,找知识,完成任务,如下结构所示: 人 → 信息 但人在用搜索引擎找信息的过程中,还需要去理解网页信息,阅读内容,并将这些网页内容通过人自身的认知模型转换为知识,转换为下一步的行动。而且每一次搜索,都要重复这一过程,付出类似的成本,这就是人找信息过程中,需要付出的边际成本。 人 → 理解认知 → 信息 但大模型的出现,将中间需要人的认知模型来理解的步骤交给大模型来完成,整个流程变成了: 人 → 大模型 → 信息 这就意味着,大模型可以部分代替人的认知能力,把繁琐的总结,汇总,提取信息的边际成本都降为0,直接可以获得人想获得的答案。这就是经过大模型改造后的搜索引擎,其效率将比传统的搜索引擎更为高效,成本更低。 更进一步,人在找到信息之后,还会有下一步的行动。传统上这些行动还需要人去做判断,思考,决策才能执行这些行动。这个阶段的边际成本也可以交给大模型来降低,如下所示: 人 → 大模型 → 信息 → 大模型 → 人 → 行动 其实上面本质上就是一种所谓的端到端闭环。举一个例子,我希望买一个空调,在搜索引擎搜索后,大模型帮我筛选了若干最新款空调的信息,并总结展示给我供我选择,但我决定选某一款空调后,大模型帮我进一步规划去哪里买,什么价格最便宜,售后服务更完善,最终高效地完成了一次人找信息,再从信息到行动的闭环。 以上,就是New Bing在这次大模型时代对传统搜索引擎做出的重构:通过在人找信息这个环境中加入大模型,将人的理解和汇总的边际成本降为0,甚至实现信息到行动的0边际成本的延伸,真正实现一个高效率的搜索引擎。 如果这样推演,我们可以扩展到更多互联网产品和平台上么?完全是可能的。我们就以另一个Web2.0时代产物,推荐系统为例,尝试推演推荐系统如何应用大模型。...
Google abseil开源项目介绍
Google abseil是Google开源的优秀C++基础库,持续维护并且持续迭代。该库的代码质量和工程化水平属于业界顶级,值得我们在实际生产中使用和学习。不仅要善于使用abseil库,还要多看abseil的文档和代码,从中学习Google业界领先的C++经验。 这里先介绍几个abseil库的经典组件: 容器 Recommendation Prefer absl::flat_hash_map or absl::flat_hash_set in most new code (see above). Use absl::node_hash_map or absl::node_hash_set when pointer stability of both keys and values is required (rare), or for code migrations from other containers with this property. Note: Do not use popularity as a guide. You will see the “node” containers used a lot, but only because it was safe to migrate code to them from other containers....
FlameGraph火焰图原理
FlameGraph是世界知名计算机性能优化专家Brendan Gregg发明的一种性能数据可视化方法。通过不同色块可交互的展示,可动态展示系统运行时的性能热点。FlameGraph如下图所示: 按照作者介绍,FlameGraph可以用于分析CPU耗时,内存分配,非CPU耗时(线程等待,调度性能),混合运行时(CPU和非CPU运行混合),以及性能对比这5种性能分析的可视化。我们常用FlameGraph生成CPU耗时分析图,用来找到服务运行性能的热点,并专项优化以节省资源成本。其实,FlameGraph也有一套标准数据格式,根据这个格式,可以在任何适合的场景中生成所需要的FlameGraph,并可视化可交互地展示和分析。 FlameGraph适合用于有调用栈的数据可视化,通过一种类似柱子的布局,加上暖色调的颜色,很像火焰一样的图案,所以被称为火焰图。以大家常用的CPU耗时火焰图为例,它的组成元素如下: 一个函数调用栈用一列方框组成,每个方框代表一个函数 y轴代表函数调用的深度,从底向上逐层递进调用,最顶端的函数代表性能采集时刻正在运行的函数,下面是它的父函数,依次类推 x轴代表不同采集栈的集合。需要注意的是x轴并不代表时间顺序,同一层的函数按照字母序从左往右排列。这样如果两个同名函数在同一层,将会被合并成一个区块。 方框的宽度代表该函数在调用栈抽样中出现的次数,如果宽度越宽,代表这个函数在栈抽样中出现的越频繁,从CPU耗时的角度来说,也表示这个函数更耗时。 如果方框宽度足够则展示函数的全名,如果不够则展示部分或者不展示,鼠标放上去可以展示全名 每个方框的背景色其实并没有什么特别,其实就是选取了一组随机的暖色调。主要是为了方便眼睛能区分不同层的方框。 一个火焰图可以用于可视化单线程,多线程,多进程甚至多主机的性能数据。也可以为每个线程生成一份独立的火焰图用于更详细的分析。 方框的宽度不仅限于表示抽样次数,也可以表示其他指标。例如宽度可以表示线程阻塞的时长,这样的一个火焰图可以很清晰的看出哪些函数在阻塞线程,以及整个线程阻塞时函数的调用栈情况 以上即是一个火焰图静态的组成部分,更为有趣的是,Gregg也为火焰图增加了互动功能,使得用户体验更佳。 火焰图本身是一个SVG文件,配合Javascript,支持鼠标悬浮,点击放大,以及搜索功能: 鼠标悬浮:当鼠标光标放置到一个方框上方时,可以展示该方框的详细数据(函数名,采样次数,以及占比) 点击放大:当鼠标点击一个方框时,火焰图按照垂直方向放大,该方框以上的函数将被放大展示,其他部分浅色处理,方便聚焦一个函数的分析 搜索:可以用Ctrl/Command+F快捷键或者右上角搜索功能,按照函数名搜索。搜索到的函数会高亮显示,还是在右下角展示所有搜索到的函数出现次数占比 火焰图生成流程是怎样的呢?按照Gregg开源的flamegraph.pl程序,生成一个火焰图只需要3步: 从perf,dtrace,gperftools等程序中获取运行时的调用栈数据 转换为折叠栈格式(Fold stacks) 调用flamegraph.pl生成火焰图 原理非常简单。所以搞懂火焰图只需要明白折叠栈是怎么回事即可。首先一个函数的调用栈可能长这样: func_c func_b func_a start_thread clone func_d func_a start_thread clone func_d func_a start_thread clone 上面展示了三次性能抽样,每次抽样从底向上显示了一个调用栈,折叠栈格式如下: clone;start_thread;func_a;func_b;func_c 1 clone;start_thread;func_a;func_d 2 其实就是将三个栈做了一个汇总,将同样的栈汇总到一起,每个函数用’;‘分隔,最后加空格和打印出现次数。 前面还可以加上程序的名称,例如cpp,如下也是一个合法的folded stacks: cpp;clone;start_thread;func_a;func_b;func_c 1 cpp;clone;start_thread;func_a;func_d 2 有了折叠栈数据,即可利用flamegraph.pl生成对应的火焰图svg文件。该文件可以嵌入到任何网页中展示,简单方便。 进一步,我们可以利用火焰图原理,生成各种场景所需要的“火焰图”,用于性能分析,数据可视化展示等功能。我们以应用层算子调度框架的性能分析为例,展示如何利用火焰图生成算子耗时可视化图。 应用层算子调度框架常见于RPC服务端的应用层,主要思想是将策略逻辑封装成若干个函数式的算子,再通过调度框架,串行或者并行调度这些算子运行,最终产出RPC的返回结果。算子调度的拓扑图有叫Tree,也有叫DAG的,也有叫图的。下图所示就是一个典型的算子调度框架的运行时拓扑图。 针对这样的算子调度,经常需要分析的问题有:哪个算子耗时最高,最长路径在哪里,哪个算子可以优化调度路径等,我们通常通过打印日志和统计最长路径等方法来分析运行时性能情况。但同样,通过火焰图也可以将运行拓扑的耗时可视化起来。 生成火焰图的关键即为生成栈的汇总数据,假设每个算子的耗时情况如下: 运行阶段 解析请求 访问A服务 访问B服务 汇总结果 产生返回数据 开始运行时间 0 10 10 24 32 结束运行时间 10 20 24 32 34 我们想象有一个抽样程序,可以按照上面的运行时图,从左往右分别垂直抽样,可以看出在不同时间段有这样几种“栈”:...
程序框架的最佳实践【翻译】
原文作者Chris Nokleberg and Brad Hawkes来自Google,Java框架开发者 原标题:程序框架最佳实践——虽然很强大,但并不适合所有人 共享代码库鼓励代码复用,实现不同团队技术的一致性并改进产品效率和质量。开发者需要选择合适的库,研究如何正确配置,最终把所有的库组装在一起。程序框架可以通过对库的默认安装和配置,来简化开发流程,提供更好的一致性,当然也损失了一些库的便利性。 框架不仅是一堆库的集合,还控制了整个程序的生命周期。确定的框架行为可以为开发开拓空间——例如,不用为每个程序应用都深入审查安全和隐私相关的代码。框架提供了跨团队和跨语言的功能一致性,同时也是更高级的自动化和智能系统的基础。 这篇文章先从框架的核心方面综述开始,然后深入到框架的优势、妥协以及我们推荐实现的最为重要的框架功能。最后,这篇文章展示了一个Google的实际框架应用案例:如何开发一个微服务平台,使得Google可以打破单一代码库的限制,以及框架如何使这一切成为可能。 什么是框架 框架和共享代码库在很多方面都类似。在Google,有两个技术原理用于区分框架和库:依赖反转和可扩展性。虽然看起来比较容易,但本文所讨论的框架很多优势,原则上都是从这两个原理衍生出来的。 IOC(依赖反转) 当从头开始开发一个程序的时候,工程师来决定程序的流程——这通常称为普通控制流。在一个基于框架的程序中,则是由框架来控制流程以及调用用户的代码——这叫做反转控制流。反转控制流有时也被引用为好莱坞原则:“别给我们打电话,我们会给你打电话”。框架控制流很好地定义了不同程序之间的标准。理想情况下,程序只需要实现它们特有的逻辑,而由框架来解决所有其他构建一个微服务所需要的各种细节。 可扩展性 扩展性是区分库和框架的第二个重要特点,它和依赖反转互相配合。因为框架的控制流是由框架负责的,唯一的改变框架运行方式的方法只能通过它暴露出来的扩展点。例如,一个服务端框架可能有一个扩展点是允许一个应用程序在每个请求来的时候运行一些代码。扩展点的模式也意味着框架的其他不可扩展性部分很固定,应用程序无法改变。 框架的好处 框架除了提供共享代码库所能提供的功能之外,还有很多好处,它们对不同的角色在不同方面都有益处。 对开发者 开发者这个角色最终决定是否使用一个可用的框架,也是最显然从框架中获益的角色。大部分的开发者的好处在于提升生产力,简便高效以及可应用最佳实践。开发者可以写很少的代码,利用内置的框架功能。由于框架处理了各种样板代码,因此他们所写的代码可以大幅简化。框架提供了合理的默认配置,消除了无意义而且浪费时间的决策判断,从而提供了充足的最佳实践。 对产品团队 除了为开发者提供生产力以外,框架也为产品团队解放了团队浪费在重复建设基础设施的资源。产品团队因此可以更关注于使他们产品与众不同的功能开发了。 框架可以让产品团队的开发与底层基础设施的变更隔离开,这也能让他们从中受益。虽然并不是普遍适用,但框架提供了额外的抽象,这意味着有些基础设施的迁移可以被当做实现细节,完全由框架维护者全权处理。 Google产品的发布需要很多团队的确认。例如,一个发布协调工程师负责审查产品安全性和有效性,同时有一个信息安全工程师检查程序应用的场景安全漏洞防范设计。当负责审查的团队熟悉框架并可以信任框架的功能保证时,框架就可以帮助简化审查流程。产品发布以后,标准化流程可以让系统更加便于管理。 对公司 从公司层面来说,常用的框架可以减少开发者上手启动一个新应用的时间,从而提升开发者的灵活性。如果一个公司有一个足够大的程序员社群,投入资源在高质量的文档和培训程序员将变得非常有价值。这也有助于吸引社群贡献文档和代码。一个被广泛应用的框架意味着,对框架很小的改进投入可以换来巨大的收益影响。 在过去一段时间,收敛框架架构可以使针对全局性的变更有广泛的应用范围。例如,如果你依赖一个统一的微服务/RPC框架,而且带宽比CPU更贵,那么框架可以基于成本权衡来优化默认的压缩参数。 框架的权衡 虽然框架有上述的多种好处,但也面临着各种权衡。 死板的框架能够掩盖创新 框架常常决定支持哪种技术方案。由于支持所有能想到的技术是不现实的,所以一个比较死板的框架也是有明显的好处的——那就是,它们更鼓励使用某种技术或者更偏好某种设计模式。 死板的框架可以极大简化一个开发者从无到有创建一个系统的工作量。当开发者有很多种方式来完成同一种任务时,他们很容易陷入是否会影响整个系统的细节决策中。对于这些开发者而言,接受一个框架所推荐的技术,可以让他们把重点放在构建他们系统的业务逻辑上。有一个普适并且一致的技术偏好对于整个公司而言更有利,虽然这个答案还不尽完美。 当然,你也必须处理程序和团队的长尾问题,有些产品需求或者团队喜好和现有的框架可能并不完全适配。框架的维护者被放在一个判断什么是最佳实践的位置上,也会需要确认一个不方便的使用案例是否“真实”,因为这个案例可能对每个用户来说都不方便。 另一个需要重点考虑的是,即使某些技术在今天看来的确是一个最佳实践,但技术也在不断快速地进化,对于框架而言也有一定的风险无法跟上技术创新的发展。采用不同的程序设计方案可能开发成本更高,因为开发者既需要学习框架实现细节,也需要框架维护者的协助。 普适性也会导致不必要的抽象 很多框架优势,例如通用的控制面板(后面解释),只有当大部分关键重要的应用都基于这个框架才有真正的意义。这样的一个框架必须足够普适来支持绝大多数用户的场景,这也意味着必须拥有丰富的请求生命周期,以及任何程序都需要的所有的扩展点。这些需求有必要在应用和底层库之间增加若干间接层,这些层也会增加学习成本以及CPU开销。对于应用开发者而言,软件栈中的更多层也会导致调试更复杂。 另一个框架潜在的缺陷是,它们需要工程师额外学习。当新来的Google员工学习如何让一个“hello world”样例运行的时候,他们经常会被他们所需要学习的技术的数量所抓狂。一个功能完备的框架也会让这个情况更糟而不会更好。 Google已经开始尝试让每个框架的核心尽量简单符合预期,让其他功能都成为可选模块,从而来解决这些问题。Google也会尝试提供配套框架的工具,可以帮助了解框架内部结构来简化调试。最终,虽然框架有一些你必须学习的成本,但你需要确保任何一个给定的框架提供了足够的好处来弥补这个成本。不同的编程语言的框架也许有不同的权衡组合,对于开发者而言这也是一个新的决策点和成本/收益权衡场景。 重要的框架特性 如上所述,反转控制和扩展性是框架最为基础的两个特点。除了这些基础能力以外,框架还需要负责若干其他功能。 标准化程序生命周期 再次重申,反转控制意味着框架拥有并且使一个应用程序的整个生命周期标准化,但这样的结构带来什么样的好处呢?我们以避免级联故障为例来说明这个问题。 级联故障是一个典型的引起系统超载的原因,Google内部也很有很多。它可能发生在分布式系统的部分服务失败,增加了其他部分失败的可能。关于级联故障更多的原因,以及如何避免他们,参考SRE(Site Reliability Engineering (O’Reilly Media, 2016))中的定位级联故障这一章。 Google的服务框架有很多内置的避免级联故障的保护功能,两个最重要的原则是: 持续运行。如果一个服务能成功响应请求,那么就应该保持。如果它能处理一部分请求但不能处理其他部分请求,那么就必须继续运行,而且响应它能服务的请求。 快速启动。一个服务必须尽量快地启动。更快地启动意味着可以快速从崩溃中恢复。服务必须避免类似启动时顺序等待RPC访问外部系统返回这样的操作。 Google生产环境给每个服务配置了一个成为健康状态(可以响应请求)的时长。如果超时了,系统则认为一个无法恢复的故障发生了,则终止这个服务进程。 有一个常见的反模式,常发生在缺乏框架的场景中:一个库创建他们管理的RPC连接,并等待连接可用。当服务端代码随着时间推移不断膨胀,你能获得一个实际有几十个这样的有序依赖的库。这样的结果是,处理初始化代码有效展开的话看起来如图1所示: 在通常的情况下,这样的代码工作很好。但因为没有任何潜在问题的提示,这样的代码会有个特殊问题。这个问题只有当一个相关的后端服务慢了或者同时挂了才会显现出来——这时关键服务启动已经延迟了。如果服务启动足够慢,那它将会在有机会处理请求之前被杀死,这会导致一场级联故障。 一种可能的改进是先创建RPC stubs,如图2所示,然后并行等待它们初始化完毕。在这种情况下,你只需等待stub初始化时间最长的那个而不是初始化时间之和。 虽然处理的并不十分完美,但这个有限的重构也说明了你需要某种在库所创建的RPC stubs之间的协调机制——它们必须可以有等待stub以及库以外的某些资源的能力。在Google的案例中,这样的事情是由服务端框架负责的,同时它也有如下功能: 通过定期拉取可用性来并行等待所有stubs可用(<1秒)。一旦配置的超时到了,即使并不是所有后端服务都准备好,服务依然可以继续初始化。 去掉用于人工或者机器可读的调试日志,而是用集成标准化监控和报警来代替。 通过一个通用的机制来支持插拔不同的资源,而不仅是RPC stubs。技术上来说,仅有返回布尔值的函数(为了“我是否准备好”)以及对应的名字,是有打印日志的必要。这些插拔点常被那些处理资源的通用库(例如:文件API)所使用;程序开发者通常只需要用这个库,就能自动地拥有这些能力 提供一个中心化的方式配置特定的关键后端服务,可以改变他们启动或者运行时的行为。 对任何一个单独的库来说,这些功能可能(也正确地)被认为是多了,但实现这些功能,能够让你在一个集中的地方实现,并在所有用这个库的后端服务都能生效。这也是有意义的一件事情。就像共享库是一种在不同应用程序中分享代码的方式,在这个定义下,框架也是一种在不同库之间共享功能的方式。 因为有类似这些的功能,SRE们更愿意支持基于框架的服务。他们也经过鼓励他们对口的程序员选择基于框架的开发方案。框架提供了一个生产规范的基础水平。这个生产规范将一堆没有关联的库关联到一起时是非常难以实现的——但也不是不可能。 标准化的请求生命周期 虽然细节取决于应用程序的类型,但很多框架支持在总的程序生命周期以外的生命周期管理。对于Google的服务端框架而言,最为重要的任务单位就是请求。除了遵循类似依赖反转的模型以外,请求生命周期管理的目标是将请求的不同方面的职责划分成独立的可扩展的代码片段。这允许程序开发者只关注于开发让他们的应用程序独一无二的实际业务逻辑。 这里有一个这样的实际使用的框架例子,它的组件片段见图3: Processors——拦截请求和返回的包。常用于打印日志,但也有一些短路请求的功能(例如,强制在整个程序中不强制执行不变量) Action——程序业务逻辑,接受请求,返回一个对象,可能有一些副作用。 Exception handler——将一个未捕捉的异常转换为一个响应对象。 Response handler——序列化一个响应对象给客户端。 虽然程序能从框架的扩展点中收益,程序代码的绝大部分都是以响应的形式执行,包括程序特有的业务逻辑。...
Memory Models读后感
读了Russ Cox关于Memory Models的三篇综述文章: Hardware Memory Models Programming Language Memory Models Updating the Go Memory Model 读完之后终于搞明白Memory Models的大体脉络。 首先在一个单核单线程的程序中,是没有内存模型问题的。随着多核多线程的引入,程序开始实现并行化提升性能,同时编译器、操作系统以及硬件针对并行化做了大量的优化,这就出现了很多新的并发数据访问问题,就需要设计规范的内存模型来解决。 内存模型分为硬件内存模型和编程语言内存模型两个层面。 其中硬件内存模型分为以Intel CPU为代表的x86-TSO模型和以ARM/POWER为代表的Relaxed Memory Model模型。前者模型中,多核对写入主存的数据提供全局序,后者模型中,多核之间不存在全局序的概念。为了协调不同硬件,又有DRF-SC(Data-Race-Free Sequential Consistency)模型,该模型是一种能让大部分硬件都能接受实现的顺序内存访问模型,这样DRF-SC可以作为硬件内存模型的统一标准。 编程语言内存模型的定义分为两个流派: 以Java,Javascript为代表的happen-before流派。特点是通过happen-before语义来定义语言的哪些方面如何实现数据的同步。该流派规范定义最早,也最为经典。Java9以后增加了弱同步,类似C++的弱同步(acquire/release)。 以C/C++,Rust,Swift等为代表的强-弱-无顺序流派。特点是分为三类同步语义,强代表前后顺序一致,和Java的happen before语义一致。弱代表在部分协调条件下的一致。无代表没有同步。 按照作者的介绍,这两个流派的内存模型都存在缺陷。Java的内存模型定义并不严格,还是有非因果、非一致的情况发生。C++的内存模型有很多未定义的情况,尤其是弱同步和无同步语义其实存在很多坑,甚至在不同硬件下运行结果可能还不一致。 Go语言采取中间流派,即只通过happen-before来定义同步语义,只支持顺序一致,不支持弱同步,不支持无同步,严格定义,同时约束编译器的优化,保证最终结果可预测,即使有未定义的情况发生,也能约束结果在有限的范围内。 最终结论是,截止目前2022年,内存模型还处于不断探索和研究的方向,Go的这个尝试也是建立在无数前人理论的基础之上,未来有待更加规范定义的模型出现。
2022 KDD论文阅读:Intelligent Request Strategy Design in Recommender System
阿里巴巴2022年KDD上发表一篇论文,介绍“瀑布流”推荐产品如何智能地选定请求时机的问题。这篇论文之前作为淘宝端智能的一项成果在知乎发布过,详见这篇文章。今年算是正式在KDD发表论文了。 阅读过程中我也一直将论文的方法和我们所做的类似的项目做对比,结论是思路是一致的,并不落后,但这篇论文的手段更加丰富,资源投入也更多,值得我们参考和学习。 背景 论文一开始先介绍什么事“瀑布流”推荐,这对于国内的移动用户而言司空见惯,就是无限下拉无限刷新推荐结果的产品交互形式,国内一般也叫feed信息流。所以需要解决的一个问题是分页请求时机的问题,就是确定在什么时机向后端发送新的推荐请求。一般是每浏览n个item后(也叫一刷)发起后端推荐请求。如果n变小,请求的频度增加,则后端服务请求的压力增加,而且很多用户本来也没有购买或者点击意愿,新增的推荐请求返回的结果也没有转换为最终的业务收益,最终白白浪费了算力。相反,如果n变大,请求的频度减少,则用户反馈延迟较长,用户实时的意愿(例如点击,购买转化等行为)没能及时反馈到后端,导致下一刷展现的还是之前缓存的推荐结果,体验较差而且不能提升产品收益。作者以淘宝App首页推荐为例,统计了每一刷的推荐结果CTR的变化情况,如下图所示: 可以明显看出每一刷头几条结果都是点击率很高,但后续衰减也是非常明显的。而且每次一刷更新点击率都有一次迅速提升,原因是新的一刷捕捉到了用户最新的兴趣点,但之后又回归到衰减。 问题和解决方案 因此,如何确定什么时机发起下次刷新是这篇论文需要解决的问题,作者给该解决方案定义了一个名词:IRSD(Intelligent Request Strategy Design),还大言不惭的说据他们所知,这是第一个在瀑布流推荐产品中研究IRSD的论文。 作者提出该问题主要有如下几点挑战: 捕捉用户兴趣点动态变化是一个难点,因为用户兴趣点是隐式变化的 不能同时观测到用户增加或者不增加下一刷请求,本质上这是一个反事实现象,所以uplift预估存在巨大的挑战。 这一点非常赞同,和我们遇到的难题类似, 不同策略其实无法在一个用户中同时存在,这也是反事实模型需要解决的问题 如何平衡新增下一刷请求的收益和带来的额外资源消耗非常有挑战性,尤其是用户数一直在动态变化 这一点和我们遇到的问题类似,如何保持QPS在系统可承载的范围内,同时拿到额外的业务收益,这也是效果-算力之间平衡的难题 为了解决以上调整,他们提出了一种新的范式,即基于Uplift算法的端智能请求框架(Uplift-based On-edge Smart Request Framework (AdaRequest))。该框架部署在端上,这样可以更极致实时地收集和分析用户行为。该框架由如下几部分组成:用户行为理解模型(CUBE),反事实请求奖励估计(CREST),动态请求规划(DRP)。其中: CUBE挖掘用户稀疏的实时行为序列,长期行为序列,以及购买交互序列。通过比较用户实时意图和历史兴趣,CUBE可以评估出用户兴趣点是否有变化 CREST目标是对treatment和control组的用户分别建模,通过因果推断(causal inference)预估出增益(uplift)。 DRP动态选择新增请求的比例,用来选取最大化的请求奖励和资源消耗之比,在有限的资源约束前提下最大化请求奖励。 最终这套AdaRequest在手机淘宝的猜你喜欢页面(也就是首页推荐)落地,和固定增加请求比例对比,有GMV提升3%的额外收益。 总的来说,论文定义并调研了瀑布流推荐场景下的IRSD问题,同时提出了AdaRequest范式解决IRSD问题。同时通过在线和离线实验量化证明了AdaRequest的有效性。 问题定义 接下来论文介绍了实现的方法。首先定义问题,从形式上IRSD问题定义如下: 其中S函数代表用户的正向反馈,F函数代表决策结果(1代表请求,0代表不请求),\(c_i^X\)代表第i个上下文环境,\(u_i^X\)代表第i个用户,\(b_i^u\)代表第i个用户的历史行为,\(X_i\)代表第i刷。R代表新增请求的资源消耗函数。因此第一个公式代表在各种上下文环境、用户、历史行为、刷次中获取的总收益大小。第二个公式代表总资源消耗约束在theta阈值以内。优化目标是找到最优的算法F,使得在满足资源消耗约束条件下,最大化业务收益。 AdaRequest框架 接下来终于一窥AdaRequest方案的整体框架,如下图所示: 虽然CUBE,CREST,DRP三大组件看起来很高级的样子,其实核心在左边的流程图中: 首先在端上实时采集用户行为,第二步CUBE将用户行为转变为用户意图变化情况,转化方式实现采用了一个神经网络来解决,产出用户浏览历史匹配度,用户当前session点击历史匹配度,用户长期点击历史匹配度,以及候选推荐item的embedding向量。第三步CREST将第二步的输出作为输入,再加上用户以及上下文特征向量,可以预估出新增请求的奖励uplift,最终交给DRP来决策是否发起新增请求。 DRP将动态选择一部分高性价比的时机发起新的请求。如果决策新增一次推荐请求,端则向后端发起请求,上传用户实时行为,后端则重新分析用户意图,返回更新的推荐item,更好地匹配用户最新的兴趣点。 因此,该论文所采用的特征体系有: 用户特征F(user) 候选item特征F(cands) 用户细粒度的历史行为特征: 用户的浏览历史F(exp) 用户当前session的点击行为F(sclk) 用户长期点击行为F(clk) 粗粒度的当前session的上下文特征F(context) 在这个特征组中,F(sclk)和F(clk)分别代表了用户实时和长期的兴趣点。F(exp)代表了上次请求曝光后用户不喜欢的item,原因是负向反馈也是一种有价值的信号。F(context)代表用户在当前session中的推荐满意程度,例如浏览的深度。论文还很贴心的把所有特征列表附在论文附录。 CUBE CUBE组件的目标是从用户实时行为中找出用户意图是否发生变化。由于推荐候选item是由后端服务根据用户历史兴趣推荐选择的,CUBE将结合用户行为,将用户刚刚交互过的item和推荐候选的item进行匹配。如果这两者不是非常匹配,意味着当前候选item并不满足用户当前意图,进一步则意味着用户意图发生了变化,因此此时如果新增一次推荐请求,能收获显著的购买uplift。 具体来说,CUBE将F(exp), F(sclk), F(clk)和F(cands)作为输入,针对特征特点做细粒度的建模。具体来说,F(sclk)特征分为用户行为序列F(sclk_beh)和item序列特征F(sclk_item),分别通过两个GRU encoder输出向量并拼接在一起。F(exp)和F(clk)类似处理,而F(cands)采用一个GRU encoding编码,并通过mean-pooling操作做fusion融合。 最终,整个模型采用attention作为将候选item和用户以及交互行为序列匹配的算子。F(cand_emb)作为attention的query输入,用于匹配候选item是否满足用户实时意图。以F(sclk)为例,第i个item是否匹配用户意图的attention评分函数如下公式所示: 可见alpha_i即当前item的匹配率在所有匹配率中的softmax归一化值。输出向量M(sclk)定义如下: 可见M(sclk)即每个item的F(sclk_embs)的加权和,代表了当前候选item是否匹配用户实时意图的程度。 上述的Match函数代表匹配分,代表推荐候选集和用户实时交互的item序列之间的相似程度,即attetion匹配函数。论文实际采用的是内积函数作为匹配函数的实现。M(exp)和M(clk)也是类似方法产出,结果的分布代表了候选item是否匹配用户不喜欢的item以及用户长期感兴趣的item。 CREST CREST模块用于预估新增一个额外的推荐请求带来的uplift收益。由于用户无法同时存在有和没有新增推荐请求的状态,因此无法反事实地观察到收益uplift的真实值groundtruth。 为了解决这个问题,作者采用两个独立的预估网络,Control Net用于预估如果没有额外的请求,购买率是多少。以及Uplift Net,用于预估如果有额外的请求,额外的增益uplift是多少。Control Net和Uplift Net加一起,可以预估出如果有额外的请求,那么购买率是多少。 为了训练这两个预估网络,论文将实验用户随机分为两组用于数据收集:control组没有额外请求,而treatment组随机发起额外请求。Control Net用control组的数据训练,而Uplift Net用control组以及treatment组的数据训练。 预估的时候,只有Uplift Net需要运行,来预估出请求reward,作为决策此次额外请求是否需要触发的基础依据。 用因果推断causality的语言来描述,CREST就是用于对新增额外请求实现CATE,描述如下: Y(0)代表没有新增请求,Y(1)代表有新增请求,CATE代表对于一个新增请求所获得的uplift值。...
并行编程之内存管理(总结)
这篇总结并行编程的三种常见的内存管理方法,三种方法如下: Reference Counting:并行编程之内存管理(一) Hazard Pointer:并行编程之内存管理(二) RCU:并行编程之内存管理(三) 三种方法各有利弊,优缺点对比如下: 引用计数(Reference Counting) Hazard Pointer RCU 读性能 低,不可扩展 高,可扩展 高,可扩展 可保护的对象数量 可扩展 不可扩展 可扩展 保护周期 支持长周期 支持长周期 用户必须限制保护周期长度 遍历是否需要重试 如果和删除冲突需要重试 如果和删除冲突需要重试 不需要 存在性保证 复杂 保证 保证 读竞争 高 无 无 对象遍历开销 CAS原子操作,内存屏障,cache missing 内存屏障 无 读遍历机制 无锁 无锁 有限制的无等待(wait free) 读引用获取 可能失败 可能失败 不会失败 内存开销 有限 有限 无限 内存回收机制 无锁 无锁 阻塞 自动回收 是 部分 部分 已经有很多C++项目,包括开源和大厂内部项目,都开始采用Hazard Pointer和RCU来实现并发数据结构,而且C++标准委员会也已经在讨论将这两个组件加入到C++26标准中。 时代在变,并行编程技术也在突飞猛进。多线程多核(multi-core)技术,甚至甚多核(many-core)技术都在飞速发展,加上各种并行编程范式的应用,可以预见到在一段时期内,并行编程将面临百花齐放,技术爆发的局面。