做一件 0.1 成就的事情

特斯拉的“宏图计划”(Master Plan)是埃隆·马斯克为公司制定的一系列长期战略目标,旨在推动全球向可持续能源转型。自2006年以来,特斯拉已经发布了三个主要篇章的宏图计划,并且马斯克在2024年宣布正在制定第四篇章,称之为“史诗级”。

第一篇章发布于2006年,概述了特斯拉的初步目标,包括生产高性能的电动跑车(Roadster)、使用赚到的资金开发人们买得起的轿车(Model S)、进一步开发更实惠的轿车(Model 3),以及提供太阳能电力产品。这些目标在过去的几年中已经基本实现。

第二篇章在2016年发布,重点在于整合能源生成和储存,创建美观的太阳能屋顶与电池产品;扩展电动汽车产品线,覆盖所有主要细分市场;开发比人类驾驶安全10倍的自动驾驶技术;以及启用汽车共享,让车辆在不使用时为车主赚钱。目前,太阳能屋顶已经上市,Model Y和Cybertruck等新车型也相继推出,自动驾驶功能正在不断进步。

第三篇章于2023年3月发布,聚焦于通过电动汽车和可再生能源实现全球经济的可持续转型。该计划提出了实现240TWh储能、30TW绿色发电、10万亿美元的制造投资的目标,并强调用不到0.2%的土地面积实现10%的世界GDP。此外,第三篇章的详细文件还透露了特斯拉未来可能推出的新车型,包括使用不同类型电池的入门车型、小型厢式车和大型巴士,以及对现有车型电池配置的更新

从上面的事情总结:

  1. 做一辆超跑 Roadster 赚钱
  2. 用赚到的钱做 Model S
  3. 继续用赚到的钱做更加实惠的 Model 3
    第一章已经完成
  4. 成为一个能源公司
  5. 成为一个自动驾驶/汽车共享公司

从目前看第二章完成了一半,后面马斯克的还会有第四章、第五章。
但是每一个章节都是建立再前面的基础上。

也就是不要想着一次就做一件大事,而是把星辰大海的事情变成 10 件递进的事情,让第一件事情是自己够得着的。

也就是我们要先做一件 0.1 成就的事情,然后不断递进。

Aider-Chat 与 DeepSeek 使用指南

简介

Aider-Chat 是一款集成了多种大型语言模型(LLMs)的代码辅助工具,能够协助开发者进行代码编辑、调试和重构。
Aider 推荐使用 GPT-4o(96.2%) & Claude 3.5 Sonnet(99.2%), DeepSeek-Code的评分在 deepseek 官方的描述里高于 Claude 3.5 Sonnet . 另外就是在国内,它可能是最合适的(便宜方便),在 aider 的描述里准确率 (97.7%)。

安装与配置

安装 Aider-Chat

  1. 确保安装了 git。各环境下的安装指南可参考 Install git
  2. 获取 OpenAI 或 Anthropic 的 API 密钥,这与 ChatGPT Plus 订阅不同。
  3. 使用 pip 安装 Aider-Chat:
    1
    2
    3
    4
    5
    6
    7
       python -m pip install aider-chat
    1. ```

    ## 设置 DeepSeek API 密钥:
    - Mac/Linux:
    ```bash
    export DEEPSEEK_API_KEY=<key>
    • Windows:
      1
      setx DEEPSEEK_API_KEY <key>
    • 重启 shell 以使设置生效。

使用 DeepSeek 模型

  1. 启动 Aider-Chat 并指定使用 DeepSeek Coder V2 模型:
    1
    aider --model deepseek/deepseek-coder

操作步骤

添加文件至聊天

  • 使用 /add 命令将需要编辑或审查的文件添加至聊天中。

代码编辑

  • 使用 /code 命令请求对代码的更改。

提交更改

  • 使用 /commit 命令将聊天外对代码库所做的编辑提交。

查看更改

  • 使用 /diff 命令展示自上一条消息以来的更改差异。

退出应用

  • 使用 /exit/quit 命令退出应用。

注意事项

  • 避免将所有文件添加至聊天,只添加需要编辑的文件,以减少令牌成本并避免混淆 LLM。
  • 使用 /help 命令解决使用 Aider 或 DeepSeek 时遇到的问题。

性能评估

根据 Aider LLM Leaderboards,DeepSeek Coder V2 在代码编辑基准测试中表现优异,正确完成的百分比为 72.9%,并且 97.7% 的编辑使用了正确的格式。

结论

Aider-Chat 结合 DeepSeek 提供了一个强大的代码编辑和开发环境,通过精确的命令和有效的交互,可以显著提高开发效率和代码质量。确保遵循最佳实践和注意事项,以充分利用这一工具的强大功能。


本文 90% 使用 AI 辅助完成

写给初学者的 Git 极简教程

Git 是一个强大的版本控制系统,它允许多个开发者在同一个项目上协作,跟踪文件的变化,并且可以在不同的时间点恢复到之前的状态。对于初学者来说,理解 Git 的基本概念和操作是非常重要的。在这篇博客中,我们将详细介绍 Git 的使用,并通过具体的例子来演示常用的 Git 命令。

Git 的三个位置

在使用 Git 时,我们需要了解三个主要的位置:本地目录、缓存区(有时也称为暂存区)和远程仓库。

1
2
3
4
5
6
7

# 记住 git 的三个位置
本地目录 <--(cache)----> .git <----> remote
add #将代码添加到缓冲区
commit --> #将代码提交到仓库
push -> 推送到远程
pull <- 从远程拉取

本地目录

这是你在计算机上存放项目文件的地方。当你开始一个新项目或者克隆一个远程仓库时,Git 会在项目文件夹中创建一个隐藏的 .git 文件夹。这个文件夹包含了所有的版本控制信息。

缓存区(.git

当你对本地目录中的文件进行修改后,这些改动并不会立即提交到远程仓库。首先,你需要使用 git add 命令将这些修改添加到缓存区。缓存区是一个临时存放改动文件的地方,你可以在这里预览将要提交的改动。

远程仓库

远程仓库是存储你的项目代码的在线平台,例如 GitHub、GitLab 或 Bitbucket。通过 git pushgit pull 命令,你可以将本地的改动推送到远程仓库,或者从远程仓库拉取其他人的改动到本地。

基本操作流程

添加(add)和提交(commit

  1. 添加改动到缓存区

    1
    git add <文件名>

    如果你想添加所有改动的文件到缓存区,可以使用:

    1
    git add .

    这里的点(.)代表了当前目录下的所有改动文件。

  2. 提交改动到本地仓库

    1
    git commit -m "你的提交信息"

    -m 选项后面跟着的是你对这次提交的描述信息,它有助于你和你的团队成员理解这次提交的目的和内容。

推送(push)和拉取(pull

  1. 推送本地提交到远程仓库

    1
    git push 远程仓库名 分支名

    例如,如果你想推送到名为 origin 的远程仓库的 master 分支,你可以使用:

    1
    git push origin master
  2. 从远程仓库拉取最新改动

    1
    git pull 远程仓库名 分支名

    这个命令会将远程仓库的最新改动拉取到你的本地目录,并自动尝试合并。

查看 Git 状态

要查看当前 Git 的状态,包括哪些文件被修改但还没有暂存或提交,可以使用 git status 命令。这个命令会列出所有的改动文件,并标明它们的状态。

理解 origin

origin 是远程仓库的默认别名。每个远程仓库都可以有一个或多个别名。你可以使用 git remote -v 命令来查看远程仓库的地址和它们的别名。

理解分支

分支是 Git 中一个非常重要的概念。它允许你在不同的线路上进行开发,而不会相互干扰。master 通常是默认的主分支,所有的开发工作都是基于这个分支进行的。

创建和使用分支

创建新分支

1
git checkout -b 新分支名

这个命令会创建一个新的分支,并自动切换到这个分支。例如,如果你想创建一个名为 feature-x 的新分支,你可以使用:

1
git checkout -b feature-x

切换分支

1
git checkout 分支名

如果你想从 feature-x 分支切换回 master 分支,你可以使用:

1
git checkout master

合并分支

当你在一个新分支上完成开发后,你可能需要将这些改动合并回 master 分支。首先,确保你已经切换到 master 分支,然后使用 git merge 命令将其他分支合并进来:

1
git merge 要合并的分支名

例如,如果你想将 feature-x 分支的改动合并到 master 分支,你可以使用:

1
git merge feature-x

通过以上步骤,你可以有效地管理你的代码变更,并且与团队成员协作开发项目。希望这篇博客能帮助你更好地理解和使用 Git!

博客和笔记不在重要

从今年年初开始,就不再写具体的博客,或者详细的笔记。只记录一些基本的、重要的细节。 GPT 的出现,让大部分的基础的技术记录不再重要,它能够很快的给你答案,甚至比你自己去查以前的笔记都要快。如果记录笔记的时间超过一年,如果这个知识点又事确定性的,比如某个知识怎么用。我可以确定问 GPT 的速度要远远高于去找以前的旧笔记。 如果事创意文字工作者,另当别论。

在好几年前给刚刚接触软件开发的一个同学讲 git 。边讲的过程边记录了文章末尾的一段命令。
于是让 AI 根据下面的内容生成一篇 blog。 生成的内容链接 写给初学者的 Git 极简教程
内容的部分除了添加了三个位置的引入,几乎没有做修改。如果是个人来写的话可能需要一个小时,并且文字描述没有那么完整。

我确信以后我们所看到的内容,大部分都是 AI 生成的。就像在刷短视频时大部分的视频都被滤镜修饰过一样。 那么我们需要的事丑陋但真实的世界,还是看上去美丽但是虚假的世界。这可能并不冲突。当需要追求根据真实时,我们会去探索真实。 但通常并不需要太真实。

大部分的内容输出,特别是具有确定性的,有很明确的步骤和方法的内容,不再需要我们写一篇博客和笔记来描述。多去记录那些不确定的内容吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#记住 git 的三个位置
本地目录 <--(cache)--> .git <----> remote
add #将代码添加到缓冲区
commit --> #将代码提交到仓库
push -> 推送到远程
pull <- 从远程拉取

git status # 查看当前git 的状态

git add . # 添加目录下所有变更
git commit -m "update" # 提交代码到仓库 update 为注释
git push origin master # 将代码推送到远程(origin)的 master 分支

git pull origin master # 从远处 mater 拉代码

# 理解 origin
origin 是远程地址的别名
git remote -v # 查看远程地址

# 理解分支
master 一般为默认主分支

git checkout -b newbanch # 创建一个名为 newbanch 的新分支,并切换到新分支

#一般我们开发会创建一个新分支修改,修改完成后在合并到主分支
git checkout master # 将分支切换回 master分支
git merge daihuanhuan # 将daihuanhuan 分支修改的内容和 master 分支合并(上一步已经切换到了master)

大模型和发现规律

科幻作家 Ted Chiang 在评价 ChatGPT 是网上所有文本的模糊图像,大模型对它的描述是对知识的压缩
大模型对大量的数据,这里的大量是指人类有史以来创建的所有文本(音频&视频)。把文本转换成一段向量,对向量进行 transformer 计算。最后得到一个模型。 模型里面记录的是什么呢?从数学上看,它是一堆的数字,或者是概率。如果把文本比作全部北京人的通勤数据,那么得到的模型就是每个路口的信息。在导航的情况下,我们是知道了每个路口的信息,然后从北京西站到西二旗。

有一部《别惹蚂蚁》的动画电影,如果在蚂蚁洞里没有方向,没有坐标。只知道蚂蚁的运动轨迹,需要怎么来确定最佳的路线呢。只要运动轨迹足够多,那么每个洞口的信息就足够清晰,这就是大模型干的事情,通过大量的输入确定某个点的的描述。 只要这样的描述足够多,那么我们就越接近真实。


印度的数学家拉马努金在数学上,给出了很多恒等式,而且很多等式在其他人看都是靠他的直觉来计算出来的。

在使用大模型的时候,大多数时候都是在寻找已经存在的知识,比如某段代码怎么写,某个文章怎么描述,我们期望找到好的题词,进而生成更加优秀的内容。

大模型通过向量,将知识映射到了多维空间,作为个人很难去理解多维空间,在科幻小说里可能会提到四维空间,比如三体:黑暗森林里面的女巫在四维空间里摘取三维空间人的大脑。星际穿越里,在四维空间里,能够看到不同三维时空。当维度升高时,事物之间的联系会变得千丝万缕。

我们生活在三维世界,理解三维世界。当维度升高时,世界就会变得我们无法理解。大模型是一个简陋的多维世界的描述,它通过多个维度,为所有存在的文字,创建了它们内在的联系。就像拉马努金脑袋里的数字之间的联系,要比我们普通人对数字的联系要多得多。

那么大模型除了是对知识的压缩外(向量空间里面的点),它还是对知识之间直觉(点和点组成的边)的描述。


知识在大模型里到底是有损还是无损的。前两天看到一篇文章,讲到大人和儿童的学习知识的方式,儿童很容易学会,而大人学习起来会很困难。里面提到人类的学习能力,其实是一种强化学习,随着年龄的增加,某些神经突触会被强化,变成一种本能的反应,当某些地方的连接被强化的同时,就会有某些地方的连接被弱化。而儿童的认知是白纸,任何的方向都很容易被强化,所以学习会比大人更加容易。

在学习一个新的知识的时候,比如大学的时候编程里面学习面相对象编程。“多态”这个名词对于新人来说是个新的名词。课本告诉我们多态是什么,其实我们是不能够理解的。甚至面向对象编程也是一个很模糊的概念,因为大学编程的实践非常的少。很难去理解这些概念,但是这并不妨碍编程的学习。

新的知识是对某个突触的强化,它是建立在现有知识里,并对现有知识的增强。从本身来说,知识被压缩到了某个点上,它是有损的。从全局来看,它又是对其他内容的扩充,它是不久是无损,而且还增加了新的内容。我们会通过旧知识来扩展和联想新知识,让点变成面。


在发现世界的规律时,当找到一个简单的规律,比如万有引力,如何通过一个简单的规律去发现更加复杂的规律,这可能才是大模型最终的能力,它不是有成百上千的直觉,而是上亿的直觉,通过这种亿级的直觉。 我们能够发现更多的规律。

三体里面宇宙社会学,如果在大模型世界里创建三条公理,能通过三条公理发现什么规律呢。在学习数学的时候,对于会有各种概念。公理、定理、推理等等。通过公理也就是不需要论证,它就是那样。通过公理推出定理,还有各种的公式. 通过万有引力公式,能够算出逃逸速度。内部的计算逻辑在大模型里是否能够,通过现有的知识,发现新的规律呢。材料科学里少许物质的变化会形成完全不同的物理特性,它们是否有一个我们尚未发现的通用公式呢。


从现实主义的角度来讲,我们需要什么样的大模型,如果不计成本的话,当然是模型越大越好。现实世界往往都是技术在成本上妥协,或者说是同样的成本提供更好的服务。比如搜索的成本,当出现使用小型机通过集群的方式代替大型机,将单个搜索的成本下降 1000 倍,当年淘宝也有去 IOE ,如果按现在的模式,整个 AI 行业基本都是在替英伟达打工。就像 20 年前整个网络都在替思科打工。 那么未来会怎么样呢。

  1. 未来同样算力的成本会下降,每一年半同样的成本算力会翻倍(摩尔定律)
  2. 本地大模型优先,在线大模型为辅,普及到每个人,交互不是 prompt ,而是自然语言。
    如果在成本不变的情况下,算力提高 10000 倍,大模型会给交互设备来带什么惊喜?
    会有新的模型来代替 transformer ,会有算力更强,能耗更低的推理设备(成本下降 10000 倍),会在 chat 之外的场景(可能是任何时间,任何地点,任何设备)。

我看可以让 AI 像大师一样作画,却没法像小孩一样去打酱油。如果 AI 是那个小孩,他知道打酱油需要 2 快钱,知道要去村东头小卖部。
每个人的期望都是突然有一天,有一个天神下凡一样的人物,突然的解决所有的问题。现实是需要慢慢打怪升级。
也许 AI 是陪伴,理解,然后成为自己,它记得你所有的往事,就像银河帝国里的克里昂大帝,永远在那里,和银河消亡。

均衡负载里的算法

在均衡负载重,轮询算法是最常用的一个算法。 通常会使用带权重的轮询(wrr).

轮询

轮询的实现非常简单,假设我们有一组节点 [a,b,c,d] ,在不带有权重的时候,只需要一个 next 变量就可以完成。
使用 next 变量记录当前的位置,下一个就是当前位置加一后与节点的长度求余。

1
2
3
4
5
6
7
next := 0 // 当前选择
.....

func rr(){
next = (next+1)%len(servers)
return next
}

带权重的轮询

通常情况下,我们都会选择带权重的轮询来作为均衡算法,带权重有几个好处:

  • 将某个机器的权重调整为 0 来进行上下线的操作
  • 上线新功能或者新机器,可以逐步调大权重,来灰度部分流量来验证功能。

假设我们有这样一组节点 [ a:10 ,b:20,c:30] , 用来表示 3 个节点和其对应的权重。
我们在做计算的时候,需要对所有的权重求出他们的最小公倍数,让他们的表示成[a:1,b:2,c:3] 这样的值。

这里涉及到一个 gcd 的算法,用来求最小公倍数

gcd

gcd 的算法非常简单,叫做辗转求余,代码如下

1
2
3
4
5
6
func gcd(a int, b int) int {
if b == 0 {
return a
}
return gcd(b, a%b)
}

具体可以参看 gcd

wrr

通过 gcd 计算得到 [a:1,b:2,c:3] 后,我们需要按照权重来选择节点。这个时候,可以将前面的节点变成这样的数组 [a,b,b,c,c,c] ,数组中的每个节点都出现的权重指定的次数,这样我们就可以通过前面 rr 里面的算法 next = (next+1)%len(servers) 来进行计算。
但是这里会出现一个问题,a,b,c 的访问在时间片上面是不均衡的,可能会导致某个时间片对某个区间的直接访问。 这就需要对数组里的内容进行打散。
在 nginx 的 wrr 算法: https://github.com/phusion/nginx/commit/27e94984486058d73157038f7950a0a36ecc6e35 保证节点选择的平滑。

算法如下: 对于节点权重 {5,1,1} ,当前一个节点被选中后,权重减去所有权重和,这里是 7 ,当开始选择时,每个节点加上自身的权重。

这里稍微有一些理论,可以想象一下,这些节点在跑道上排成一队,在选择之前,每个人向前走自己的权重步,如果谁在最前面的话,就能够被选中,被选中的人需要向后退权重和的步数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 a  b  c
0 0 0 (initial state)

5 1 1 (a selected) 第一次选择了 a ,那么 a 的权重 - 7
-2 1 1 第一次选择后的结果

对第一次选择结果加 5 1 1 得到 3 2 2
3 2 2 (a selected) 第二次选择 a ,那么 a 的权重 -7 得到 -4 2 2
-4 2 2 如此反复整过过程。

1 3 3 (b selected)
1 -4 3

6 -3 4 (a selected)
-1 -3 4

4 -2 5 (c selected)
4 -2 -2

9 -1 -1 (a selected)
2 -1 -1

7 0 0 (a selected)
0 0 0

这个算法能够很好的使算法能够均衡。

hash

使用 hash 作为均衡负载算法,会应用到很多地方,通常情况下我们会根据请求的一些值来作为均衡的 key。

特别是一致性的 hash 算法,处在网络请求的很多位置都会用到,可能所用到的 hash 算法不同,但是本质都是为了保证同一个数据包能够发送到一个位置。
通常会根据不同的需求,对 源 ip、源端口 、目标 ip、源端口、目标端口、协议号 这五元组来做 hash 算法。 比如以下的场景

  • 数据包到达网口,网卡多队列
  • cpu 支持 DUMA 的时,需要让相同的数据包进来在一个核上处理
  • 四层均衡负载中保证 tcp/udp 是到同一个后端
  • 七层均衡负载中保证同一个客户端到同一个后端处理

meglev

Maglev算法是Google开发的一种网络负载均衡器。在很多新的四层均衡负载中喜欢使用,比如 facebook 的 katran。
maglev 使用的一个大的槽位,通过一致性 hash 来将计算分布到槽位上。
使用时固定了槽位,查询效率为 O(1).

https://github.com/zhangweidev/meglevgo

NAT小记

NAT小记

NAT 的分类

  • 全锥形: 一旦内部主机端口对(iAddr:iPort)被NAT网关映射到(eAddr:ePort),所有后续的(iAddr:iPort)报文都会被转换为(eAddr:ePort);任何一个外部主机发送到(eAddr:ePort)的报文将会被转换后发到(iAddr:iPort)

    外部主机不限制 ip 和端口

  • 限制锥形:一旦内部主机端口对(iAddr:iPort)被映射到(eAddr:ePort),所有后续的(iAddr:iPort)报文都会被转换为(eAddr:ePort);只有 (iAddr:iPort)向特定的外部主机hAddr发送过数据,主机hAddr从任意端口发送到(eAddr:ePort)的报文将会被转发到(iAddr:iPort)。

    外部主机限制ip不限制端口

  • 端口限制锥形: 一旦内部主机端口对(iAddr:iPort)被映射到(eAddr:ePort),所有后续的(iAddr:iPort)报文都会被转换为(eAddr:ePort);只有(iAddr:iPort)向特定的外部主机端口对(hAddr:hPort)发送过数据,由 (hAddr:hPort)发送到(eAddr:ePort)的报文将会被转发到(iAddr:iPort)。

    外部主机限制 ip 和端口

  • 对称型: NAT网关会把内部主机“地址端口对”和外部主机“地址端口对”完全相同的报文看作一个连接,在网关上创建一个公网“地址端口对”映射进行转换,只有收到报文的外部主机从对应的端口对发送回应的报文,才能被转换。即使内部主机使用之前用过的地址端口对去连接不同外部主机(或端口)时,NAT网关也会建立新的映射关系。

    外部主机的响应包才能发送

STUN、TURN、ICE

https://developer.aliyun.com/article/243540

STUN: 为终端提供一种能够获取自己经过NAT映射后的地址.

客户端向公网 STUN 服务器发送 Binding Request ,服务器收到后获取公网 IP:PORT,附加在 Binding Request 返回给客户端. 

TURN: TURN 作为通讯中间人,由服务器负责两方的数据转发.

ICE: 一种框架,可以整合现有的NAT穿透协议,尽可能的找到NAT穿透的数据通道.

打洞过程

  • 两个客户端处于同一 NAT 设备后

Untitled.png

当A向集中服务器发出消息请求与B进行连接,集中服务器S将B的外网地址二元组以及内网地址二元组发给A,同时把A的外网以及内网的地址二元组信息发给B。A和B发往对方公网地址二元组信息的UDP数据包不一定会被对方收到,这取决于当前的NAT设备是否支持不同端口之间的UDP数据包能否到达(即Hairpin转换特性),无论如何A与B发往对方内网的地址二元组信息的UDP数据包是一定可以到达的,内网数据包不需要路由,且速度更快。A与B推荐采用内网的地址二元组信息进行常规的P2P通信。

  • 两个客户端处于不同 NAT 设备后

Untitled.png

同上一个例子一样, A 和 B 得到了对方的外网 ip:port

当 A 往 NAT-B 发送 UDP 消息,经过 NAT-A ,并在 NAT-A 上生成会话表项,根据NAT类型可知除了全锥形NAT,NAT-B 认为设备 A 得消息未授权外网消息,会丢弃该数据包.

这时B设备向A发送一个UDP消息. NAT-B 上也会生成一个到NAT-A 的会话表项.

此时 NAT-A 和NAT-B 都有了对方在外网的二元组,打开了 A 和 B 之间的洞.A 和 B 可以开始数据传输.

  • 两个客户端位于两层 NAT 设备后

Untitled.png

当出现多层级的 NAT 时(这是我们常见的类型),我们通过外网服务器S来打洞,可能存在某个 NAT是最优的选择,但是外网服务器S并不能够观察到,只能够选择离服务器S最近的NAT-C来打洞.

NAT 设备在空闲状态下会对转换表进行清理,比如一些家用的路由设备保存 NAT 时间大概是2-3分钟.某些设备可能短的只有 20s .为了维持可以通过心跳包的方式来维持连接.在连接超时后进行重新打洞.

http://www.52im.net/thread-542-1-1.html

如何创建一个小的容器镜像

如何创建一个小的容器镜像

对 golang 程序上线到了容器,在使用 scratch 的时候遇到了一个小小的问题,顺手对编译做了一些整理. 以下:

编写一个简单的Go应用程序,例如 gin 的例子 **main.go**:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"net/http"

"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

例子1: Dockerfile 编译

使用容器来编译运行程序

1
2
3
4
5
6
7
# 使用官方的 Golang 镜像作为基础镜像
FROM golang:1.20
WORKDIR /app
COPY . .
RUN go build -o main
EXPOSE 8080
CMD ["./main"]

通过构建的到了容器大小为 1.16GB

1
docker-golang-build-example1              latest               cd34e318087d   40 seconds ago   1.16GB

这个时候会容器的内容又一些大了. 可以看看 https://hub.docker.com/_/golang

golang:1.20 是基于 Debian 来作为基础镜像,本身就很大.

例子2: 使用 apline

我们可以看到提供了 alpine 作为基础镜像的包 golang:<version>-alpine ,只需要将 FROM 后面的镜像加上 alpine 即可

1
2
3
4
5
6
7
# 使用官方的 Golang alpine 镜像作为基础镜像
FROM golang:1.20-alpine
WORKDIR /app
COPY . .
RUN go build -o main
EXPOSE 8080
CMD ["./main"]

Untitled.png

使用 alpine 作为基础镜像会存在一些问题, 它使用了 musl libc 来代替 glibc ,小的镜像和稳定性的讨论会有一些不同的意见: https://news.ycombinator.com/item?id=10782897

镜像的大小从 1.16G 变成了 570M

例子3: 分段编译,使用 busybox

在编译过程中, go build 会拉取依赖,使用分段将编译和运行的镜像分开. golang 在运行时不需要编译环境,这里我直接使用 busybox 来作为基础镜像,同时例子2中使用 alpine 会存在一些问题,所以我们使用 busybox 的 glibc 的包.

1
2
3
4
5
6
7
8
9
10
11
FROM golang:1.20 AS build
WORKDIR /app
COPY . .
RUN go build -o main

# 最终阶段
FROM busybox:glibc
WORKDIR /app
COPY --from=build /app/main /app
EXPOSE 8080
CMD ["./main"]

Untitled.png

编译后得到了 15.5M 的镜像.

例子4: 静态编译,使用 scratch

在容器镜像中有一个 0M 的基础镜像 scratch ,它是所以镜像的基础.

容器本质上是基于Linux内核的Namespace、Cgroups和Union FS等技术对进程进行封装隔离的操作系统层面的虚拟化技术。

我们可以通过静态编译的方式,基于 scratch 来运行. 构建Go应用程序,并确保它是静态链接的。可以使用以下命令来构建:

1
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app main.go

在编译中添加了 -a -installsuffix cgo 两个条件,并指定了CGO_ENABLED=0:

-a : 强制重新构建
-installsuffix cgo: 指示编译器将生成的目标文件和包安装到一个特定的目录中,以便将与C语言绑定的包(使用**cgo**工具)与其他Go包区分开。
CGO_ENABLED=0 : cgo 工具标识为禁用

对 例子三 中的 dockerfile 做编译做调整,这个Dockerfile分为两个阶段:构建阶段和最终阶段。在构建阶段,它使用了官方的**golang镜像来构建Go应用程序,然后在最终阶段使用了scratch**基础镜像来创建最终的容器。

1
2
3
4
5
6
7
8
9
10
# 构建阶段
FROM golang:1.20 AS build
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app

# 最终阶段
FROM scratch
COPY --from=build /app/app /app
CMD ["/app"]

Untitled.png

编译后 docker-golang-build-example4 的大小从 15.5M 西江到了 11.2M, 这一点点的差值几乎就是 busybox 的存储占用.

要注意的问题

使用 scratch 镜像是一个空镜像,所以需要特别注意一些依赖和系统调用.

涉及到依赖时,需要解依赖,比如证书依赖:

COPY --from=builder /etc/ssl/certs/ /etc/ssl/certs/

涉及到系统调用,无法使用 scratch ,就需要考虑 apline 的方案.

例子5 加快编译速度

当把运行镜像的大小降下来后,还存在另一个问题,运行 go build 的时候都需要重新拉一遍golang 的环境. 可以通过挂在缓存的位置来减少应用程序编译时的时间.比如 golang 的 pkg目录 $GOPATH/pkg .在编译前提前做好 pkg 缓存,将缓存复制到目录中.

假设已经准备好了缓存目录 pkg. 在 例子4 中添加 COPY pkg/* /go/pkg/ 来加快编译速度.

1
2
3
4
5
6
7
8
9
10
11
# 构建阶段
FROM golang:1.20 AS build
WORKDIR /app
COPY pkg/* /go/pkg/
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app

# 最终阶段
FROM scratch
COPY --from=build /app/app /app
CMD ["/app"]

这个例子可能对 golang 的意义不是特别大, 对一些依赖比较多的 node 程序的 node_modele 目录缓存就很有用.

总结

通过四种方式,不断的降低了容器镜像的大小. 但是四种方式并没有特别的优劣之分,在不同的场景下,可以通过不同的策略来做镜像编译的方式.

  • 使用更小的镜像 apline 、busybox、scratch 来降低镜像大小
  • 使用分阶段构建来减少
  • 使用缓存来加快编译

在没有多阶段编译的时期,还有将多条命令写成一条的方式来降低容器的层数,或者借助一些工具来合并层.

以上例子放在了 github ,可以使用 docker-compose 来验证

docker-golang-build-example

使用 rust 和 golang 体验 wasm

使用 rust 和 golang 体验 wasm

本来是来记录使用 rust 来做 wasm 的开发.但是整个过程相比 golang 体验 wasm 的开发实在是太流畅了,几乎是跟着文档走就结束了. 整体的感受是相比 golang , rust 的 wasm 的工具链非常完善.

跟着文档,几个命令就把整体的流程走通,没什么可记录的.

Rust

https://rustwasm.github.io/docs/book/

工具准备

  • yarn global add wasm-pack

https://rustwasm.github.io/wasm-pack/installer/

  • cargo install cargo-generate
  • npm install npm@latest -g

项目

创建项目: cargo generate --git https://github.com/rustwasm/wasm-pack-template

创建后设置项目名称,比如’rest_wasm_example’就完成了项目的创建,文件的 src 中包含了 lib.rs 导入了 alert函数, 导出了 greet 函数.

构建: wasm-pack build

Golang

相比 golang 需要使用 tinygo 来实现相关的开发,而且工具链没有 rust 完善. tinygo 的整个的文档就在下面的文档中,在官方代码中还有几个示例

https://tinygo.org/docs/guides/webassembly/wasm/

这个也足够我们搞清楚使用 tinygo 来完成 wasm 的开发.

感受

tinygo 相比 golang 生成出来的 wasm 文件要小得多. 但是和 rust 相比还是大了一些

在我运行生成的例子中,rust 生成的是 330 字节, tinygo 生成的 56KB.

rust 工具链完善,生成的 js 文件也更加工程化.用来和前端项目做集成会方便一些.

wasm 在浏览器里运行,有内存的限制.wasm 引擎预先分配一段内存,被称为线性内存. 在运行过程中,wasm 只能访问这段内存.着相当于一个虚拟的空间,在写程序时要时刻考虑内存占用的问题.

wasm 虽然是为浏览器出现的,但是它体现出的跨平台的特性,可能会成为一种基础底层的能力.

wasm 的出现,让浏览器作为载体,可能会出现真正的基于云的操作系统.

运行时场景思考

  • 从运行时的角度看

wasm 的运行时场景,有一些像 java 或 C# 的运行时, 比如在编译的时候会编译成为字节码,然后字节码在放到运行时里去运行.

wasm 的运行时就是 java 的 JRE 或者 c# 的 CLR . 可以通过任何的语言编译成为 wasm 后运行.java、c# 跨平台的特性变成的所有语言的特性. 可以想象 java 能做的事情, wasm 都能做.

  • 从容器的角度看

在看一些技术文章时,也有从容器的角度看看待 wasm , 假设运行时是一个安全的容器.那么 wasm 还欠缺什么呢.

程序运行时需要的资源有两种

  1. 计算资源:指的是 cpu 或者 gpu

    如何将计算资源标准化后供提供到 wasm 里面. 或者说 wasm 的运行时,如何提供底层计算资源的分配和限制.

  2. 存储资源:指的是内存

    wasm 提供了线性内存分配方式, 在 web 中是没有问题的, 但是在服务端作为密集运算的时候,可能会存在性能的问题.

  3. 输入输出端

    输入输出对于计算机语言已经有一个很好的虚拟,所有的语言都会提供一个 io 的基础库来负责输入输出. wasm 的运行时需要做一些输入输出的标准化的工作. 比如网卡(虚拟网络)、硬盘(虚拟文件)、键盘(虚拟输入输出)等等.

bookmark

在看各种运行时时,有些运行时已经做了上述的事情,比如 wasmedge 运行时为操作系统资源(例如文件系统、套接字、环境变量、进程)和内存空间提供隔离和保护.

从支持的语言看 wasmer 更多一些. wasmtime 作为字节码联盟,的到了一些大公司的支持, wasmedge 是 CNCF 的项目. 从资源的角度来看,可能 wasmtime 的发展会更好一些.

其他资源

https://wasmbyexample.dev/home.en-us.html 一些例子

https://developer.mozilla.org/zh-CN/docs/WebAssembly

https://wazero.io/ 使用 go 实现的 wasm 运行时

https://wasmtime.dev/ rust 实现的运行时,字节码联盟

https://wasmedge.org/ 运行时, CNCF 项目,

https://github.com/wasmerio/wasmer rust运行时

https://github.com/wapm-packages

机器学习记录

机器学习记录

不同的维度,看到的事物的形态也是不同的. 横看成岭侧成峰,远近高低各不同.

最简单的公式

$$
f(x)=XW+b
$$

有一堆数据,能够知道 x 和 y .现在我们通过一些方法来求出 Wb ,这样当有未知的 $X_n$ 通过 W 和 b 就能够求出 $f(x_n)$ 的值.

当把维度增加,公式就变成了

$$
f(x_1..xn)=X_1W_1+…X_nW_n+b
$$

https://microsoft.github.io/ai-edu/基础教程/

反向传播:

https://www.jiqizhixin.com/graph/technologies/7332347c-8073-4783-bfc1-1698a6257db3

https://zhuanlan.zhihu.com/p/40761721

正向传播: 传播信号从输入层到隐藏层到输出层,一层一层的传播,最后得到结果.

反向传播: 输出结果和真实结果存在误差,通过误差反向的传递给前面的个层,来调整网络的参数.

$$
f(x) = ax_1+bx_2
$$

如果 f(x) = z 而我们计算的值为y 那么误差为 m=z-y,那么我们需要通过误差来调整参数 a 、b 的值. 当误差值不断往前传播,最后通过误差计算出新的权重的过程.

https://cloud.tencent.com/developer/article/1897045

CNN: 卷积神经网络

http://arthurchiao.art/blog/cnn-intuitive-explanation-zh/

卷积的四种操作

  • 卷积 : 通过小矩阵对输入矩阵进行运算,学习图像特征,通过 ilter 保留像素空间关系.

Untitled.png

  • 非线性:卷积后通过一个称为 ReLU 的运算
  • 池化或降采样: 对卷积+ ReLU 的特征做降采样,比如44 降为22

Untitled.png

  • 分类/全连接

    Untitled.png

比如上图做两次卷积后,在做全连接.

卷积+降采样作为特征提取,全连接作为分类器

RNN 循环神经网络

http://fancyerii.github.io/books/rnn-intro/

Untitled.png

$x_t$ 为 t 时刻的输入

$s_t$ 为 t 时刻的隐状态,它可以看作是网络的记忆

$s_t = f(Ux_t+Ws_{t-1})$

使用上一层的输出和当前的输入作为当前的输入.

上一层的输入是前面权重计算,层数越多,前面层的影响越小(权重会越来越小),存在短期记忆的问题.

另外的

  • 双向 RNN
  • 深度双向 RNN
  • LSTM
  • GRU

transformer

https://cloud.tencent.com/developer/article/1897045

Transformer的优势在于,它可以不受梯度消失的影响,能够保留任意长的长期记忆。而RNN的记忆窗口很短;LSTM和GRU虽然解决了一部分梯度消失的问题,但是它们的记忆窗口也是有限的。

encoder-decoder 结构

https://luweikxy.gitbook.io/machine-learning-notes/seq2seq-and-attention-mechanism

给定的输入 encoder 后计算得到中间语义,使用 decoder 解码.

Untitled.png

attention 注意力机制

不在将输入编码成固定长度 .而是根据当前生成的新单词计算新的$C_i$

Untitled.png

transformer结构

https://www.tensorflow.org/tutorials/text/transformer?hl=zh-cn

https://erickun.com/2020/04/11/Transformer-原理-源码分析总结-Tensorflow官方源码/

Untitled.png

Transformer 结构分为编码器和解码器两部分. 编码器有 N 个层,解码器也有 N 个层.

Encoding

  • Positional Encoding

    https://www.cnblogs.com/emanlee/p/17137698.html

    transformer 是将所有词一起输入,并行操作,所以需要提供位置信息.

    位置嵌入的维度为[max sequence length ,embedding dimension] (输入的最大单句长度 、 词的维度)

    Untitled.png

    在论文中使用了 sin 和cos 函数的线性变换提供了模型的位置信息

    Untitled.png

    d:输出嵌入空间的维度

    pos:输入序列中的单词位置,0≤pos≤L-1

    这个公式的意义是,对于每个位置 pos 和每个维度 ,计算出对应的角度速率(angle_rate),用于位置编码中的计算。这样可以保证不同位置和不同维度的编码具有不同的特征,有助于模型更好地学习序列的位置关系。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    def get_angles(pos,i,d_model,n):
    # pos/10000^(2*i/d_model)
    denominator = np.power(n, 2*(i/d_model))
    return pos/denominator

    def getPositionEncoding(seq_len,dim,n = 10000):
    PE = np.zeros((seq_len, dim))
    for pos in range(seq_len):
    for i in range(int(dim/2)):
    PE[pos,2*i] = np.sin(get_angles(pos,i,dim,n))
    PE[pos,2*i+1] = np.cos(get_angles(pos,i,dim,n))
    return PE

    疑问:

    使用 sin cos 交替的意义,奇数位和偶数位的位置描述不同? 如果直接使用随机数可行吗?

    2*i/d_model 在值域上的变化为 0-1-2,中值为1 ,对应的指数函数的变化不同,表

    示前半段的位置信息和后半段的位置信息不同?

在小说降临里面,对语言的描述是立体的,电影里面是一个环,而信息在环上延伸.最后学会了七支桶的语言,从而能够预测未来. 环描述了位置,也描述了信息.位置信息可能是相对的.

Multi- HeadAttention

https://imzhanghao.com/2021/09/15/self-attention-multi-head-attention/

https://www.cvmart.net/community/detail/5563

https://mp.weixin.qq.com/s/cJqhESxTMy5cfj0EXh9s4w

https://zhuanlan.zhihu.com/p/338817680

  • self Attention 自注意力机制

    Untitled.png

    1.初始化Q,K,V

    首先初始化三个权重矩阵$ W_Q 、 W_K、 W_V$ 然后将 X_embedding 与这三个权重矩阵相乘,得到 Q、K、V

    Untitled.png

    1. 计算 Self-Attention Score

    Untitled.png

    1. 对Self-Attention Socre进行缩放和归一化,得到Softmax Socre

    Untitled.png

    1. Softmax Socre乘以Value向量,求和得到Attention Value

Untitled.png

Multi-Head Attention的作用

将 Scaled Dot-Product Attention 的过程做 H 次(H=8)

把输出合并起来

$$
head_i = Attention(Q_i,K_i,V_i),i=1…8 \ MultiHead(Q,K,V) = Concact(head_1,…head_8)W^o
$$

Untitled.png

  • Add&Norm

    Add & Norm 由 Add 和 Norm 两个部分组成

    $$
    LayerNorm(X+MultiHeadAttention(X)) \
    LayerNorm(X+FeedForward(X))
    $$

    X 表示 MultiHeadAttention 或 FeedForward 的输入

    ADD 指的是 X + MultiHeadAttention(X) ,是一种残差链接,让网络指关注当前差异的部分.

Untitled.png

LayerNorm

https://zhuanlan.zhihu.com/p/492803886

对维度进行均值方差计算,LN 对 hidden 的维度做归一化操作.

Untitled.png

  • Feed Forward

    Feed Forward 是两个全连接层 ,第一层激活函数为 Relu ,第二层不使用激活函数

    $$
    max(0,XW_1+b_1)W_2+b_2
    $$

    X 是输入,Feed Forward 最终的到输出矩阵的维度与 X 一致.

encode

Untitled.png

Untitled.png

encode 的部分由 Multi-Head Attention , Add & Norm, Feed Forward, Add & Norm

decode

Untitled.png

解码器由三层结构组成

  • 第一层由带掩码(masked)的多头注意力层和一个残差连接

    Masked 操作目的是将后面的内容做掩盖,因为在生成过程中,生成了第i个字,才能生成第 i+1 个. 通过 masked 可以防止第i个字知道i+1的内容.

Untitled.png

Untitled.png

Untitled.png

Untitled.png

通过 masked 后的输出内容只有前 i 个字符的信息.

  • 第二层也是一个多头注意力层和一个残差连接

根据 Encoder 的输出 C 计算得到 K,V ,根据上一次输出的 Z 计算出 Q.进行多头注意力计算和残差连接. (通过掩码的 Q 去 Encode 的 K V 里查询出可能的内容. )

  • 第三层是前溃全连接层和一个残差连接.

计算方式与 encode 中的一致

最后线性层和 Softmax层

最后通过 Softmax 预测所有的单词

Untitled.png

链接

google develop 的llm简介: https://developers.google.com/machine-learning/resources/intro-llms?hl=zh-cn