2024年11月

前言:
大型语言模型(LLMs)的发展历程可以说是非常长,从早期的GPT模型一路走到了今天这些复杂的、公开权重的大型语言模型。最初,LLM的训练过程只关注预训练,但后来逐步扩展到了包括预训练和后训练在内的完整流程。后训练通常涵盖监督指导微调和对齐过程,而这些在ChatGPT的推广下变得广为人知。

自ChatGPT首次发布以来,训练方法学也在不断进化。在这几期的文章中,我将回顾近1年中在预训练和后训练方法学上的最新进展。

关于LLM开发与训练流程的概览,特别关注本文中讨论的新型预训练与后训练方法

每个月都有数百篇关于LLM的新论文提出各种新技术和新方法。然而,要真正了解哪些方法在实践中效果更好,一个非常有效的方式就是看看最近最先进模型的预训练和后训练流程。幸运的是,在近1年中,已经有四个重要的新型LLM发布,并且都附带了相对详细的技术报告。

• 在本文中,我将重点介绍以下模型中的**苹果的 AFM智能基础语言模型 **预训练和后训练流程:

• 阿里巴巴的 Qwen 2

• 苹果的 智能基础语言模型

• 谷歌的 Gemma 2

• Meta AI 的 Llama 3.1

我会完整的介绍列表中的全部模型,但介绍顺序是基于它们各自的技术论文在arXiv.org上的发表日期,这也巧合地与它们的字母顺序一致。

  1. 苹果的苹果智能基础语言模型(AFM)

我很高兴在arXiv.org上看到苹果公司发布的另一篇技术论文,这篇论文概述了他们的模型训练。这是一个意想不到但绝对是积极的惊喜!

2.1 AFM 概述

在《苹果智能基础语言模型》论文中,研究团队阐述了为“苹果智能”环境在苹果设备上使用而设计的两个主要模型的开发过程。为了简洁,本节将这些模型简称为AFM,即“苹果基础模型”。

具体来说,论文描述了两个版本的AFM:一个是30亿参数的设备上模型,用于在手机、平板或笔记本电脑上部署,另一个是更高能力的服务器模型,具体大小未指明。

这些模型是为聊天、数学和编程任务开发的,尽管论文并未讨论任何编程特定的训练和能力。

与Qwen 2一样,AFM是密集型的LLM,不使用混合专家方法。

2.2 AFM 预训练

我想向研究人员表示两大致敬。首先,他们不仅使用了公开可用的数据和出版商授权的数据,而且还尊重了网站上的robots.txt文件,避免爬取这些网站。其次,他们还提到进行了使用基准数据的去污染处理。

为了加强Qwen 2论文的一个结论,研究人员提到质量比数量更重要。(设备模型的词汇大小为49k词汇,服务器模型为100k词汇,这些词汇大小明显小于使用了150k词汇的Qwen 2模型。)

有趣的是,预训练不是进行了两个阶段,而是三个阶段!

1.核心(常规)预训练

2.持续预训练,其中网络爬取(较低质量)数据被降权;数学和代码被增权

3.通过更长的序列数据和合成数据增加上下文长度

                      AFM模型经历的三步预训练过程概述。

让我们更详细地看看这三个步骤。

**2.2.1 预训练I:核心预训练 **

核心预训练描述了苹果预训练流程中的第一阶段。这类似于常规预训练,AFM服务器模型在6.3万亿个标记上进行训练,批量大小为4096,序列长度为4096个标记。这与在7万亿标记上训练的Qwen 2模型非常相似。

然而,对于AFM设备上的模型更为有趣,它是从更大的64亿参数模型(从头开始训练,如上一段所描述的AFM服务器模型)中提炼和修剪得到的。

除了“使用蒸馏损失,通过将目标标签替换为真实标签和教师模型的top-1预测的凸组合(教师标签分配0.9的权重)”之外,关于蒸馏过程的细节不多。我觉得知识蒸馏对于LLM预训练越来越普遍且有用(Gemma-2也使用了)。我计划有一天更详细地介绍它。现在,这里是一个高层次上如何工作的简要概述。

知识蒸馏的概述,其中一个小型模型(此处为AFM设备3B模型)在原始训练标记上训练,外加来自一个更大的教师模型(此处为6.4B模型)的输出。注意,a)中的交叉熵损失是用于预训练LLM的常规训练损失(有关如何实现常规预训练步骤的更多细节,请参阅我的《从零开始构建大型语言模型》一书第5章)。

如上所示,知识蒸馏仍然涉及在原始数据集上训练。然而,除了数据集中的训练标记之外,被训练的模型(称为学生)还从更大的(教师)模型接收信息,这比没有知识蒸馏的训练提供了更丰富的信号。缺点是你必须:1)首先训练更大的教师模型,2)使用更大的教师模型计算所有训练标记的预测。这些预测可以提前计算(需要大量存储空间)或在训练期间计算(可能会减慢训练过程)。

2.2.2 预训练II:持续预训练

持续预训练阶段包括了一个从4096个标记到8192个标记的小幅上下文延长步骤,使用的数据集包含1万亿个标记(核心预训练集是其五倍大)。然而,主要关注点是使用高质量数据混合进行训练,特别强调数学和编码。

有趣的是,研究人员发现在这种情况下蒸馏损失并无益处。

2.2.3 预训练III:

上下文延长 第三阶段预训练只涉及1000亿个标记(第二阶段使用的标记的10%),但代表了更显著的上下文延长到32768个标记。为了实现这一点,研究人员用合成的长上下文问答数据增强了数据集。

                                    AFM预训练技术概述。

**2.3 AFM后训练 **

苹果公司在后训练过程中似乎采取了与预训练相似的全面方法。他们利用了人工标注数据和合成数据,强调优先考虑数据质量而非数量。有趣的是,他们没有依赖预设的数据比例;相反,他们通过多次实验精调数据混合,以达到最佳平衡。

后训练阶段包括两个步骤:监督指令微调,随后进行数轮带有人类反馈的强化学习(RLHF)。

这个过程中一个特别值得注意的方面是苹果引入了两种新算法用于RLHF阶段:

  1. 带教师委员会的拒绝采样微调(iTeC)

  2. 带镜像下降策略优化的RLHF

鉴于文章的长度,我不会深入这些方法的技术细节,但这里有一个简要概述:

iTeC算法结合了拒绝采样和多种偏好调整技术——具体来说,是SFT、DPO、IPO和在线RL。苹果没有依赖单一算法,而是使用每种方法独立训练模型。这些模型然后生成响应,由人类评估并提供偏好标签。这些偏好数据被用来在RLHF框架中迭代训练一个奖励模型。在拒绝采样阶段,一个模型委员会生成多个响应,奖励模型选择最佳的一个。

这种基于委员会的方法相当复杂,但应该相对可行,特别是考虑到涉及的模型相对较小(大约30亿参数)。在像Llama 3.1中的70B或405B参数模型上实现这样的委员会,无疑会更具挑战性。

至于第二种算法,带镜像下降的RLHF,之所以选择它是因为它比通常使用的PPO(邻近策略优化)更有效。

                                        AFM后训练技术总结。

**2.4 结论 **

苹果在预训练和后训练的方法相对全面,这可能是因为赌注很高(该模型部署在数百万甚至数十亿的设备上)。然而,考虑到这些模型的小型性质,广泛的技术也变得可行,因为一个30亿参数的模型不到最小的Llama 3.1模型大小的一半。

其中一个亮点是,他们的选择不仅仅是在RLHF和DPO之间;相反,他们使用了多种偏好调整算法形成一个委员会。

同样有趣的是,他们在预训练中明确使用了问答数据——这是我在之前文章中讨论的《指令预训练LLM》。

总之,这是一份令人耳目一新且令人愉快的技术报告。

来源:晓飞的算法工程笔记 公众号,转载请注明出处

论文: Exploring the Benefit of Activation Sparsity in Pre-training

创新点


  • 研究了激活属性在预训练过程中的变化,发现
    Transformer
    在大多数预训练过程中表现出稀疏激活,同时激活相关性随着训练的进行而不断演变。
  • 提出了可切换的稀疏-密集学习(
    Switchable Sparse-Dense Learning

    SSD
    ),在预训练过程中自适应地在基于专家混合(
    Mixtures-of-Experts
    ,
    MoE
    )稀疏训练和传统的密集训练之间切换,充分利用稀疏训练的效率,避免了稀疏训练的静态激活相关性。
  • 与密集训练相比,
    SSD
    在相同模型规模下实现了可比的性能,并降低了预训练成本。
  • 使用
    SSD
    训练的模型可以直接作为
    MoE
    模型用于稀疏推理,并且在推理速度上可实现与密集模型相同的性能,速度提升可达
    \(2\times\)

内容概述


预训练的
Transformer
本质上具有稀疏激活的特征,即每个
token
只有一小部分神经元被激活。尽管稀疏激活在后训练(推理)方法中得到了探索,但其在预训练中的潜力仍未被发掘。

论文研究了
Transformer
在预训练过程中的激活情况,发现模型在预训练的早期阶段变得稀疏激活,随后在这种稀疏状态中稳定下来。尽管这表明稀疏激活是一个普遍现象,但激活模式仍然是动态的:对于某个输入,被激活的神经元集合在不同的预训练阶段之间存在变化。因此,针对预训练的稀疏训练方法应该适应激活模式的变化。

基于这些观察,论文提出了可切换稀疏-密集学习(
Switchable Sparse-Dense Learning

SSD
),利用稀疏激活现象加速
Transformer
的预训练,并提高推理效率。

SSD
包含两种训练阶段:

  • 原始的密集训练,有助于激活模式的演变。
  • 稀疏训练,旨在在激活模式稳定之后有效优化模型参数。

在整个预训练过程中,
SSD
在这两个阶段之间切换。具体而言,当激活稀疏性增加并且激活模式变得稳定时,通过将密集模型转换为稀疏激活专家混合模型(
Sparsely-activated Mixture-of-Experts

SMoE
)来切换到稀疏训练,从而有效近似原始密集模型。此外,最终的密集模型熟悉稀疏计算形式,这对后续的稀疏推理是有利的。

与传统的密集训练相比,
SSD
在相同模型大小和更少的预训练成本下实现了可比的性能,在
FLOPs
上实现了高达
\(1.44\times\)
的加速。此外,使用
SSD
预训练的模型可以作为
SMoE
模型进行推理,而无需任何额外训练,同时将前馈网络的推理时间减少了高达
\(2\times\)
,且性能与密集预训练模型相当。

SSD


SSD
专注于加速
Transformer
中的前馈网络(通常占总计算量的
60%
以上),通过在预训练阶段切换稀疏和密集模式实现加速。

  1. 在稀疏计算下,模型被转换为
    SMoE
    模型,相较于其原始形式,计算成本更低。稀疏激活使得
    SMoE
    模型能够模拟原始模型,从而在效率和效果之间取得平衡。
  2. 在密集计算期间,所有模型参数都被计算和优化,以实现更好的性能。

最终模型恢复为密集配置,以充分利用模型容量。此外,最终模型也适配稀疏计算,可以直接用于高效的稀疏推理,而无需任何额外的训练。

在密集计算中,前馈网络(
FFNs
)通过以下方式计算:

\[\begin{equation}
\small
\text{FFN}({\mathbf{x}}) = {\mathbf{W}}_o \sigma({\mathbf{W}}_i {\mathbf{x}} + {\mathbf{b}}_i) + {\mathbf{b}}_o,
\end{equation}
\]

在稀疏计算中,前馈网络(
FFNs
)被均分为
\(N\)
个专家,并以
SMoE
的方式进行计算,

\[\begin{equation}
\small
\text{FFN}_{\text{SMoE}}({\mathbf{x}}) = \sum_{n=1}^N \alpha_n {\mathbf{W}}_{o,n} \sigma({\mathbf{W}}_{i,n} {\mathbf{x}}),
\end{equation}
\]

使用门控网络来评估每个专家对于给定输入
\({\mathbf{x}}\)
的重要性,选择重要性分数最高的
\(K\)
个专家来计算输出。未被选择的专家的
\(\alpha_n\)
被设置为
\(0\)

密集转换为稀疏

  • 神经元聚类

使用平衡的
k-means
聚类将
\({\mathbf{W}}_i\)
的行(每一行代表一个特定神经元)聚类成
\(N\)
组,假定具有相似权重的神经元更可能被同时激活。

基于聚类结果
\({\mathbf{s}} \in \mathbb{R}^{d_{\text{ff}}}\)
,其中包含每个神经元的相应专家索引,将权重矩阵
\({\mathbf{W}}_i, {\mathbf{W}}_o\)
分割为
\(N\)
个子矩阵
\({\mathbf{W}}_{i,n}, {\mathbf{W}}_{o,n}\)

为了使转换更平滑,使用前一个
checkkpoint
的聚类中心作为当前
checkkpoint
聚类的初始化。这个简单的策略通常提供更好的结果,小于随机初始化中心的簇内平方和(
WCSS
)。

为了避免局部最优(特别是训练早期),进行两次聚类,一次使用随机初始化,另一次使用来自前一个
checkkpoint
的初始化,并选择更好的结果。形式上,第
\(j\)

checkkpoint
的聚类结果
\({\mathbf{s}}_j\)
由以下公式计算:

\[\begin{equation}
\small
{\mathbf{s}}_{j} = \min_{{\mathbf{s}} \in \{f({\mathbf{W}}_i), f({\mathbf{W}}_i, {\mathbf{s}}_{j-1})\}} \text{WCSS}({\mathbf{W}}_i, {\mathbf{s}}),
\end{equation}
\]

其中
\(f({\mathbf{W}}_i)\)

\(f({\mathbf{W}}_i, s_{j-1})\)
分别是使用随机初始化和来自前一个
checkkpoint
的初始化的聚类结果。

  • 专家选择

使用输入
\({\mathbf{x}}\)
与聚类中心之间的相似性作为重要性评分,以选择前
\(K\)
个专家。形式上,第
\(n\)
个专家的重要性评分由以下公式计算:

\[\begin{equation}
\small
\alpha_n = {\mathbf{x}}^\top {\mathbf{c}}_n,\quad{\mathbf{c}}_n = \frac{N}{d_{\text{ff}}} \sum_{m=1}^{\frac{d_{\text{ff}}}{N}} {\mathbf{W}}_{i,n}^m,
\end{equation}
\]

其中
\({\mathbf{W}}_{i,n}^m\)

\({\mathbf{W}}_{i,n}\)
的第
\(m\)
行,而
\({\mathbf{c}}_n\)
是第
\(n\)
个专家的聚类中心。

  • 转换时机

通过监控激活模式的变化以确定过渡时间,其中激活模式的相似性反映了激活模式的变化速率,当激活稀疏度较高且激活模式稳定时进行转换。

具体而言,设定一个阈值
\(\tau\)
,当两个连续
checkkpoint
之间的激活模式相似性大于
\(\tau\)
时切换到稀疏训练。

稀疏转换为密集

SMoE
模型的性能往往落后于具有相同参数的稠密模型,为了最优地利用模型容量并避免稀疏计算形式的过拟合,在训练期间战略性地多次回归到稠密训练。


\(K=N\)
时,
SMoE
计算与稠密计算是一致的,因此过渡到稠密计算是平滑的。通过连接所有专家的权重矩阵来进行此转换,从而获得稠密权重矩阵,同时忽略门控网络。

这个过渡使得全参数优化成为可能,有效缓解了稀疏训练引起的表示崩溃问题,并促进了激活模式的演变。

  • 转换时机

为了实现可控的速度比率,建议保持稀疏训练步骤与所有训练步骤之间的常数比率
\(r\)
。此外,为了确保最终模型可以进行稠密使用,在训练结束时采用稠密训练。

主要实验




如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

前言

在OSG中,osgUtil::Optimizer是一个非常重要的工具类,它提供了一系列优化场景图的方法,以提高渲染性能和效率。


Demo

请添加图片描述

在笔者的pc上,优化前优化后渲染交互没啥区别,可能是使用的是一个没有分部件的STL大模型原型,内存32GB,以下为cpu和显卡:
在这里插入图片描述

没优化前
在这里插入图片描述

默认优化后,cpu占用率提升,可能是用于优化计算了:
在这里插入图片描述

开启所有优化选项,cpu占用率提升,gpu降低约1%~%2
在这里插入图片描述


osgUtil::Optimizer


功能概述

osgUtil::Optimizer是一个强大的优化工具,它提供了多种优化策略,包括几何体合并、节点空间位置分组、相邻LOD节点合并等。以下是几个常用的优化功能:

  • MERGE_GEOMETRY:将多个几何体合并成一个,以减少渲染时的几何体数量,提高渲染效率。这一功能在处理大规模场景时尤为重要,可以显著减少渲染时间。
  • SPATIALIZE_GROUPS:根据节点的空间位置进行分组,便于后续进行裁剪和LOD(Level of Detail)划分。这有助于减少不必要的渲染,提升性能。
  • COMBINE_ADJACENT_LODS:合并相邻的LOD节点,以简化场景图结构,提高渲染效率。
  • 其他优化:osgUtil::Optimizer还提供了许多其他优化功能,如简化几何体、生成法线、生成Delaunay三角网等,以满足不同场景的需求。


使用方法

使用osgUtil::Optimizer进行场景图优化的过程很简单。创建一个osgUtil::Optimizer对象,然后调用其optimize()方法,并传入要优化的场景图节点即可。


使用场景

  • 大规模场景渲染:在处理大规模场景时,osgUtil::Optimizer可以通过合并几何体、优化节点结构等方式,显著提高渲染性能。
  • 实时仿真:在实时仿真应用中,性能是至关重要的。osgUtil::Optimizer可以帮助开发者优化场景图,减少渲染时间,提高仿真效率。
  • 虚拟现实:在虚拟现实应用中,场景复杂度和细节程度通常较高。osgUtil::Optimizer可以通过优化场景图结构,提高渲染效率,从而提升用户体验。
  • 可视化:在可视化应用中,数据通常以三维图形的形式呈现。osgUtil::Optimizer可以帮助开发者优化场景图,提高渲染速度,使数据更加直观地呈现出来。


osg::Optimizer使用步骤


步骤一:添加头文件

在这里插入图片描述

#include <osgUtil/Optimizer>


步骤二:创建实例

在这里插入图片描述

// 步骤二:创建实例
osgUtil::Optimizer optimizer;


步骤三:优化场景(Node类型下的都可以)

在这里插入图片描述

// 步骤三:优化场景
// optimizer.optimize(pGroup.get());
optimizer.optimize(pGroup.get(), osgUtil::Optimizer::ALL_OPTIMIZATIONS);


Demo源码

osg::ref_ptr<osg::Node> OsgWidget::getOptimizerNode()
{
// 其他demo的控件
updateControlVisible(false);

osg::ref_ptr<osg::Group> pGroup = new osg::Group();

// 加载支持stl格式插件
osgDB::Registry::instance()->addFileExtensionAlias(".stl", "stl");

// 加载模型
{
osg::ref_ptr<osg::Node> pNode;
QString filePath = "T:/CVN76.STL";

pNode = osgDB::readNodeFile(filePath.toStdString());
if(!pNode.get())
{
LOG << "Failed to openFile:" << filePath;
}

pGroup->addChild(pNode);
}

#if 1
// 优化场景
{
// 步骤一:添加头文件
// #include <osgUtil/Optimizer>

// 步骤二:创建实例
osgUtil::Optimizer optimizer;

// 步骤三:优化场景
// optimizer.optimize(pGroup.get());
optimizer.optimize(pGroup.get(), osgUtil::Optimizer::ALL_OPTIMIZATIONS);
}
#endif

return pGroup.get();
}


工程模板v1.38.0

在这里插入图片描述

基本类型的响应式数据

在 Vue 3 中,
ref
是一个函数,用于创建响应式的数据。它主要用于处理基本类型(如数字、字符串、布尔值等)的数据响应式

当我们调用 ref 函数时,会返回一个包含一个 .value 属性的对象。这个对象会被转换成 Proxy 对象,通过拦截 getter 和 setter 操作,实现对 .value 属性的监听。当读取 .value 属性时,会触发 getter操作,将 .value 属性的值返回。当修改 .value 属性时,会触发 setter操作,将新的值赋给 .value 属性

<script setup lang="ts" name="UserInfo">
// 导入ref
import {ref} from "vue"




//  需要让哪个数据是响应式的,就给哪个数据使用ref
let age = ref(18)

let name = ref("vue3")

// 不需要响应式
let address:string  = "Beijing"


// ref返回的是一个响应式对象,里面的value是具体的值
// 在template模版中使用 例如age,不需要age.value,模版会自动.value解析
// 但是在ts、js代码中使用,需要.value

function setAge(){
  age.value = 20  // 将value改成20,响应式修改

}


</script>
<template>
  {{ age }}
  {{ name }}
  {{address}}
  <button @click="setAge">修改年龄</button>
</template>

ref也可以用于处理对象和数组,但对于复杂类型,
reactive
函数可能更加合适

<script setup lang="ts" name="UserInfo">
import {ref} from "vue"
//  响应式对象数据
// ref处理对象的原理-底层调用了reactive
let info = ref({name: "vue3", age: 18})


// 修改age
function setAge() {
  info.value.age = 20
}


</script>

<template>
  {{ info.age }}
  {{ info.name }}
   <button @click="setAge">修改年龄</button>
</template>

对象类型的响应式数据

reactive
是一个用于创建响应式对象的函数。它主要用于处理复杂的数据类型,如对象和数组,使得这些数据成为响应式数据,即当数据的内容发生变化时,与之绑定的组件模板会自动更新

当我们调用 reactive 函数时,会将传入的普通 JavaScript 对象转换为一个 Proxy 对象,通过拦截 getter 和 setter 操作,实现对整个对象的监听。当读取对象的属性时,会触发 getter操作,返回对应属性的值。当修改对象的属性时,会触发 setter操作,将新的值赋给对应的属性

<script setup lang="ts" name="UserInfo">
// 导入reactive
import {reactive} from "vue"

// 需要让哪个对象是响应式的,就给哪个对象使用ref
//  响应式对象
let info = reactive({name:"vue3",age:18})

// 非响应式对象
let ext = {address:"Beijing"}

// 修改age
function setAge(){
  info.age = 20

}


</script>
<template>
  {{ info.age }}
  {{ info.name }}
  {{ext.address}}
  <button @click="setAge">修改年龄</button>
</template>

ref和reactive的区别

  • ref可以用来定义基本数据类型、对象数据类型

  • reactive只能用来定义对象数据类型

  • 若需要一个基本类型的响应式数据,必须使用ref

  • 若需要一个响应式对象,层级不深,ref、reactive都可以。

  • 若需要一个响应式对象,且层级较深,推荐使用reactive

  • ref创建的变量必须使用.value

  • reactive重新分配一个新对象,会失去响应式,可以使用Object.assign整体替换

    let info = reactive({name: "vue3", age: 18})
    
    // info = {name:"vue2"},给info重新分配对象,会失去响应式
    
      // info = reactive({name:"vue2"}) 不能使用该方式重新分配响应式对象,使用该方式后,不是原先的响应式info是一个新的响应式info,页面不更新
    
      // 使用这种方式重新分配对象
      Object.assign(info,{name:"vue2",age:20})
    
  • 如果是使用ref创建的响应式对象,重新分配一个对象,可以直接分配

    let info = ref({name: "vue3", age: 18})
    // 直接赋值替换,也是响应式的,通过.value拿到替换,.value拿到的就是响应式的
    info.value = {name:"3",age:20}
    
    // 响应式基本数据类型不能直接这样修改,需要通过.value
    // 使用该方式相当于创建了一个新的响应式,而不是原先的响应式,页面是跟原先的响应式关联
    let count = ref(10)
    count = ref(20)
    

toRefs和toRef

  • 作用:将一个响应式对象中的每一个属性,转换为
    ref
    对象。
  • 备注:
    toRefs

    toRef
    功能一致,但
    toRefs
    可以批量转换
let info = reactive({name: "vue3", age: 18})

// 解构赋值
let {name, age} = info

function setInfo() {

  //页面没有响应式变化
  // 变化的数据是解构出来的age,而不是info.age
  age += 1
}

// 解构赋值
// 底层实现是将每一个属性 使用ref进行转换,都是ref对象
// toRefs可以批量转换
let {name, age} = toRefs(info)

function setInfo() {

  
	// 页面数据可以响应式变化
  age.value += 1
}
// toRef只能转换单个
let age = toRef(info,age)

计算属性

根据已有数据计算出新数据(和
Vue2
中的
computed
作用一致)

计算属性是有缓存的,多个场景使用一个计算属性,计算数据不发生变化,则只计算一次

  1. 只读计算属性
// 导入computed计算属性,是一个函数
import {computed, ref} from "vue"


let name = ref("vue")
let age = ref(20)


// 计算属性 
// 只读取的计算属性,不能通过info=xxx这种方式修改info
let info = computed(() => name.value + "-" + age.value)
  1. 可读可修改计算属性
// 导入computed计算属性,是一个函数
import {computed, ref} from "vue"


let name = ref("vue")
let age = ref(20)


// 计算属性
let info = computed({
  get() {
    return name + "-" + age
  },
  set(value) {
    // value需要是 xx-xxx的数据格式
    // 将数据用-切割
    const [str1,str2] = value.split("-")
    // 将数据赋值给name和age  触发重新计算  然后生成新的info
    name.value = str1
    age.value = Number(str2)
  }
})

监视属性

  • 作用:监视数据的变化(和
    Vue2
    中的
    watch
    作用一致)
  • 特点:
    Vue3
    中的
    watch
    只能监视以下四种数据:
  1. ref
    定义的数据。
  2. reactive
    定义的数据。
  3. 函数返回一个值(
    getter
    函数)
  4. 一个包含上述内容的数组
ref基本数据类型

直接写数据名监视,监视的是该数据value值的改变

// 导入watch
import {watch, ref} from "vue"


let name = ref("vue")
let age = ref(20)


// 监视
// 要监视的属性不需要写.value,回调函数(该属性新值,该属性旧值)
const wa = watch(name, (newValue, oldValue) => {
  console.log("新数据",newValue,"旧数据",oldValue)

  // 如果新数据 的长度大于等于10的时候,停止监视
  if (newValue.length >=10){
    // 原理:watch函数会返回一个函数。这个返回的函数实际上是一个用于清理或停止监视操作的句柄
    wa()
  }

})
ref对象类型数据
  1. 监视的是对象的【地址值】,若想监视对象内部的数据,要手动开启深度监视

    // 导入watch
    import {watch, ref} from "vue"
    
    
    let info = ref({name: "vue", category: "js"})
    
    
    // 监视的是对象的内存地址的值
    // info.value = {}的时候会触发,修改里面某个属性不好触发
    watch(info, (newValue, oldValue) => {
      // 如果修改整个对象,newValue是新对象,oldValue是旧对象
      console.log(newValue, oldValue)
    })
    
  2. watch的第三个参数是一个配置对象,deep、immediate等等

    watch(info, (newValue, oldValue) => {
      // 如果修改某个属性,newValue, oldValue两个都是新值,因为他们是同一个对象
      // 如果修改整个对象才会是两个对象
      console.log(newValue, oldValue)
    
    
      // deep开启深度监视,修改里面的某个属性 、修改整个对象 都会触发
      // immediate在watch函数创建的时候立即执行一次回调函数
    },{deep:true,immediate:true})
    
reactive对象数据类型

监视reactive对象,默认开启了深度监视

import {watch, ref, reactive} from "vue"


let info = reactive({name: "vue", category: "js"})



watch(info, (newValue, oldValue) => {
  // newValue, oldValue不管是修改整个对象还是某个属性,都是新值
  // 因为修改某个属性值 or Object.assign修改,地址值都没变,都是从同一个对象拿数据,都是新值
  console.log(newValue, oldValue)


  // 默认开启了深度监视
})
ref或reactive对象数据的某个属性
  1. 监视响应式对象中的某个属性,且该属性是基本类型的,要写成函数式(可监视的第三种数据,能返回一个值的函数)

    // reactive 和ref创建都可
    let info = reactive({
      name: "vue", category: "js", address: {
        phone: 110,
        email: "123213"
      }
    })
    
    
    // 监视info对象的name属性,函数式
    watch(() => info.name, (newValue, oldValue) => {
      // new是新值,old是旧值
      console.log(newValue, oldValue)
    })
    
  2. 监视响应式对象中的某个属性,且该属性是对象类型的,可以直接写也可以写函数式,推荐函数式

    // reactive 和ref创建都可
    let info = reactive({
      name: "vue", category: "js", address: {
        phone: 110,
        email: "123213"
      }
    })
    
    
    
    // 直接编写
    // address中的某个属性发生变化,可以监视,但是整个对象替换,不会触发监视,即便开启深度监视
    watch(info.address, (newValue, oldValue) => {
      console.log(newValue,oldValue)
    
    })
    

    // 函数式
    // address中的某个属性发生变化,可以监视,整个对象替换,也可以触发监视
    // 若是对象监视的是地址值,需要关注对象内部,需要手动开启深度监视
    watch(() => info.address, (newValue, oldValue) => {
      console.log(newValue, oldValue)
    
    },{deep:true})
    
监视多个数据
// 监视多个数据,可以放在一个数组里面
watch([() => info.address,()=>info.name], (newValue, oldValue) => {
  // newValue和oldValue 是前后的整个info对象
  console.log(newValue, oldValue)

},{deep:true})

watchEffect

立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数


watch
对比
watchEffect

  1. 都能监听响应式数据的变化,不同的是监听数据变化的方式不同

  2. watch
    :要明确指出监视的数据

  3. watchEffect
    :不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性)

// 引入watchEffect
import {watchEffect, ref, reactive} from "vue"

let info = reactive({
  name: "vue", category: "js", address: {
    phone: 110,
    email: "123213"
  }
})


let count = ref(0)



// 初始化的时候会首先执行一次,相当于watch的immediate:true
const wa = watchEffect(()=>{
// 函数中用到哪些属性,vue会分析监视哪些属性
  if (info.name.length >10){
    alert("长度大于10")
    // 停止监视
    wa()
  }

  if (count.value === 10){
    alert("count")
    wa()
  }

})

标签中的ref属性

用于注册模版引用

<template>
  <!--  与vue2一样-->

  <!--  用在普通DOM标签上,获取的是DOM节点 -->
  <h1 ref="t">title</h1>
  <!--  用在组件标签上,获取的是组件实例对象-->
  <UserInfo ref="user"></UserInfo>

  <button @click="cli">按钮</button>
</template>
<script setup lang="ts">

import {ref} from "vue";

let t = ref() // ref=t的DOM节点对象,可以直接操作该DOM
let user = ref() // ref=user的组件实例对象


</script>

通过ref获取的组件实例对象,有一个保护措施,在父组件要访问子组件的哪些数据,需要通过defineExpose显示的指定

// 在子组件引入defineExpose
import {ref,defineExpose} from "vue"

// 在子组件显示指定的数据,在父组件通过ref拿到的组件实例对象才可以fang'w
defineExpose([name,age])

props

和vue2的props原理一样,主要是语法上的区别

  1. 父组件

    <script setup lang="ts" name="App">
    import UserInfo from './components/UserInfo.vue'
    
    // 引入自定义类型约束 前面要+ type
    import { type UserList} from "@/types";
    
    import { reactive} from "vue";
    
    
    let users = reactive<UserList>([
          {id: "001", name: "1", age: 1},
          {id: "002", name: "2", age: 2},
          {id: "003", name: "3", age: 3},
        ]
    )
    
    
    </script>
    

    <template>
    
      <!-- 传递users-->
      <UserInfo :list="users"></UserInfo>
    
    </template>
    
  2. 子组件-不约束类型

    <script setup lang="ts" name="UserInfo">
    import { type UserList} from "@/types";
    
    // import {defineProps} from "vue"
    // 看视频教程defineProps需要导入,但是导入后使用有报错,然后看资料不需要导入,不确定是不是和版本有关系
    // 不约束类型
    defineProps(["list"])
    // 如果使用变量接收defineProps,返回值是{传递的key:传递的value}
    
    </script>
    

    <template>
    
      <ul>
        <li v-for="user of list" :key="user.id">{{ user.name }}</li>
      </ul>
    
    </template>
    
  3. 子组件-约束类型

    // 接收list参数,类型是UserList
    defineProps<{ list: UserList }>()
    
  4. 子组件-可选以及设置默认值

    // 使用withDefaults设置默认值 
    withDefaults(
        // 第一个参数defineProps接收参数,?可行参数是ts语法
        defineProps<{ list?: UserList }>(),
        // 第二个参数是一个对象,给可选参数设置默认值
        {
          list: () => [{id: "001", name: "1", age: 1}]
        }
    )
    
    

vue3的生命周期钩子

<script setup lang="ts" name="UserInfo">


// setup函数是创建阶段,在setup语法糖里相当于进入了创建阶段

import {onBeforeMount, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, onUpdated} from "vue";

// 挂载前
onBeforeMount(() => {
  console.log("挂载前")
})


// 挂载完毕
onMounted(() => {
  console.log("挂载完毕")
})
// 更新前
onBeforeUpdate(() => {
  console.log("更新前")
})
// 更新完毕
onUpdated(() => {
  console.log("更新完毕")
})

// 卸载前,等同于vue2的销毁
onBeforeUnmount(() => {
  console.log("卸载前")
})

// 卸载后

onUnmounted(() => {
  console.log("卸载后")
})
</script>

自定义hooks

  • 本质是一个函数,把
    setup
    函数中使用的
    Composition API
    进行了封装,类似于
    vue2.x
    中的
    mixin

  • 自定义
    hook
    的优势:复用代码, 让
    setup
    中的逻辑更清楚易懂

  • 使用use作为hooks文件名的前缀(非强制,社区惯例)

  1. 编写hooks文件

    import {ref, onMounted, computed} from "vue";
    
    // 整体hooks逻辑写在一个函数里
    export default function () {
    
        // 定义响应式数据
        let info = ref<number>(1)
    
    
        // 获取数据
        function getDetail() {
            return info
        }
    
        // 修改数据
        function setDetail(value: number) {
            info.value = value
        }
    
        // 可以写钩子方法
        onMounted(() => {
            console.log("挂载完毕")
        })
    
    
        // 可以写计算属性
        let doubleInfo = computed<number>(() => {
            return info.value * 2
    
        })
    
    
        // 其他的监视属性等等vue方法属性都可以在这里编写
        // 将这些方法组合成一个hooks,然后提供给组件使用
    
        // 向外部提供东西
        return {getDetail, setDetail,doubleInfo}
    
    }
    
  2. 使用hooks

    import useDetail from "@/hooks/useDetail";
    
    
    const {getDetail,setDetail,doubleInfo} = useDetail()
    getDetail()
    setDetail(6)
    

看前提醒:这一系列笔记完全是按照我的思考顺序写的,中间可能会绕弯路

定义

为了避免概念的混淆,我先在这里作一下(仅适用于本文的)名词的解释:

  • 引导程序
    /
    boot程序
    :特指磁盘MBR或者VBR扇区中存放的程序
  • 加载器
    /
    loader程序
    :指由引导程序加载执行的程序,用于加载操作系统的内核
  • 引导
    :指从BIOS调转到引导程序到真正执行内核之间的整个流程

背景

在研究引导程序时,我发现很多书籍和文章都设定系统从软盘引导,因此引导扇区的代码通常是直接写死,从第一个软盘读取加载器(loader)。为了迁移到硬盘引导,顺便改成一个通用的引导,我参考了IBM的BIOS文档进行设计。

BIOS中关于引导的规定

由于引导扇区仅有
512字节
大小,所以需要有引导程序加载另外的loader程序来打破大小的限制,否则仅凭这点空间什么都做不了。首先,boot程序无法确定引导位于哪个磁盘上也就无法加载loader,很多的书也都是默认在
第一个软盘
上来写的,如果需要读取磁盘中的loader就需要
磁盘的设备号

但考虑到现在的引导都没有这种问题,肯定是由解决方案的。传统BIOS引导都遵循着IBM的规范,所以我决定到IBM的文档中去寻找答案。

于是经过一番快速搜索,我在BIOS的中断调用功能中找到了可能的答案:

根据IBM BIOS文档,
INT 19
中断功能的目的是启动
Bootstrap Loader
。在调用该中断后时,BIOS会设置以下寄存器:

  • CS = 0000H,IP = 7C00H
    :指定引导扇区的内存加载地址。
  • DL
    :保存引导设备的驱动器号。

看到这个
7C00H
就感觉DNA动了,虽然文档表明这似乎是用于从指定驱动器重新引导的功能,但我们不妨大胆的猜测在初次启动
Boot程序
是也是同样的状态,因为重新从磁盘引导时磁盘上的
引导程序
大概率也是按照直接启动的情况写的。

实践验证

为了验证引导过程的具体行为,我使用了
QEMU
配合
GDB
进行调试,在0x7C00处设置断点并观察寄存器的值。测试结果如下:

  • 当参数指定从
    软盘(0号软盘)
    引导时,
    DL = 0x00
  • 当参数指定从
    硬盘(0号硬盘)
    引导时,
    DL = 0x80

由于在BIOS中:

  • 软盘驱动器号范围为
    0x00 ~ 0x7F
  • 硬盘驱动器号范围为
    0x80 ~ 0xFF

由此可以确定,启动时,引导设备的驱动器号会保存在
DL寄存器
中。

引导程序设计

基于以上分析,我的引导程序设计思路如下:

  1. 保存设备号
    :在引导程序运行时,直接保存DL的值,作为后续加载程序(loader)的驱动器号。这使得引导程序能够同时兼容新老磁盘。
  2. 读取磁盘功能:
    • 支持软盘的
      标准CHS
      (柱面-磁头-扇区)寻址模式读取。
    • 支持硬盘的
      扩展LBA
      (逻辑块地址)模式读取,适配
      超过CHS寻址范围

      约8GB
      )的大容量硬盘。

设计思路

  1. 引导程序(boot)的设计

    • 位置
      :引导程序位于硬盘的第0扇区,同时兼容MBR(主引导记录)。
    • 结构
      :从扇区偏移0x1BE开始的64字节保留用于保存分区表信息。
  2. 加载程序(loader)的功能

    • 遍历分区表,寻找唯一具有可引导标志的分区。
    • 读取启动磁盘参数,选择CHS或LBA模式,读取目标分区的第0扇区。
    • 仅支持
      FAT文件系统
      (也许兼容FAT系列文件系统)。
    • 检测FAT的
      BPB
      (BIOS参数块)结构,根据BPB信息定位到根目录的数据扇区。
    • 循环读取文件目录项,找到名为
      KERNEL.ELF
      的文件(FAT短目录项文件名均为大写),并加载到内存中。
  3. 内核加载

    • 通过支持FAT文件系统的设计,内核文件的大小可以动态变化,并且能够被文件管理系统灵活修改。
    • 读取内核文件到内存后,跳转执行,完成引导流程。

注意事项

在实现过程中需要特别注意:

  • 使用
    DX寄存器
    时,应提前保存设备号,以免在后续操作中丢失。
  • 引导程序应尽可能简洁,以节省空间并提高兼容性。

结局

咳咳,看到这个小标题就能说明不会有什么好下场。实际上原本经过好一番折磨终于在16位模式下写出了加载内核的汇编代码,先是因为16位模式的寻址范围限制在
1MB
导致BIOS加载的时候直接卡住,原本想着留着做纪念的结果又不小心把他给删了TAT