2024年10月

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

论文: Training-Free Model Merging for Multi-target Domain Adaptation

创新点


  • 对域适应的场景解析模型中的模式连通性进行了系统的探索,揭示了模型合并有效的潜在条件。
  • 引入了一种模型合并技术,包括参数合并和缓冲区合并,适用于多目标域适应任务,可应用于任何单目标域适应模型。
  • 在数据可用性受限的情况下,也能达到与使用多个合并数据集进行训练相当的性能。

内容概述


论文研究的是场景理解模型的多目标域适应(
MTDA
)。虽然之前的方法通过领域间一致性损失取得了可观的结果,但它们通常假设可以不切实际地同时访问所有目标领域的图像,忽略了数据传输带宽限制和数据隐私等问题。鉴于这些挑战,论文提出了一个问题:如何在不直接访问训练数据的情况下合并在不同领域独立适应的模型?

对此问题的解决方案包含两个部分,即合并模型参数和合并模型缓冲区(即归一化层统计数据)。在合并模型参数方面,模式连通性的实证分析意外地表明,对于使用相同的预训练主干权重训练的单独模型,线性合并就足够了。在合并模型缓冲区方面,使用高斯先验来建模现实世界分布,并从单独训练模型的缓冲区中估计新的统计数据。

论文的方法简单而有效,取得了与数据组合训练基线相当的性能,同时消除了访问训练数据的必要性。

方法


以往的方法假设,在适应阶段能够同时访问所有目标领域图像的非实际假设。相反,论文方法的流程包括两个不同的阶段:

  1. 单目标域适应阶段,分别训练适应于各个目标领域的模型。简单地采用最先进的无监督域适应方法
    HRDA
    ,利用各种主干架构,如
    ResNet
    和视觉
    Transformer
  2. 模型合并阶段(主要关注点),专注于将这些适应后的模型合并在一起以创建一个稳健的模型,而不需要访问任何训练数据。该方法包含模型的两个关键组成部分:参数(即可学习层的权重和偏置)和缓冲区(即归一化层的运行统计信息)。

参数合并

论文通过对比实验发现,当从相同的预训练权重开始时,域适应模型能够有效地过渡到多样的目标领域,同时在参数空间中保持线性模式连接。因此,这些训练模型之间的简单中点合并可以生成在两个领域中都具有鲁棒性的模型。

缓冲区合并

缓冲区,即用于批归一化(
BN
)层的运行均值和方差,与领域有密切关系,因为它们封装了特定领域的特征。现有方法主要处理在同一领域内对两个训练于不同子集的模型的合并,而论文研究在完全不同目标领域中训练的两个模型的合并,因此缓冲区合并的问题变得不再简单。

BN
层的引入是为了缓解内部协变量偏移的问题,即输入的均值和方差在经过内部可学习层时发生变化。在这种背景下,基本考虑是后续的可学习层预期合并的
BN
层的输出遵循正态分布。由于输出的
BN
层保留了输入符合高斯先验的归纳偏见,因此可以从
\(\mathbf{\Gamma}_A\)

\(\mathbf{\Gamma}_B\)
中获取的值来估计
\(\boldsymbol{\mu}^{(i)}\)

\([\boldsymbol{\sigma}^{(i)}]^2\)
。首先获得来自该高斯先验的数据点的均值和方差的两个集合,以及这些集合的大小,共同利用这些值来估计该分布的参数。

当将合并方法扩展到
\(m (m \geq 2)\)
个高斯分布时,可以按如下方式计算已跟踪批次的数量
\(n^{(i)}\)
、均值的加权平均
\(\boldsymbol{\mu}^{(i)}\)
和方差的加权平均。

\[\begin{equation}
\label{m-buffer-merging-n-and-mean}
\begin{split}
n^{(i)} =& n^{(i)}_1 + n^{(i)}_2 + \cdots +n^{(i)}_M, \\
\boldsymbol{\mu}^{(i)} =& \frac{1}{n^{(i)}} (n^{(i)}_1 \boldsymbol{\mu}^{(i)}_1 + n^{(i)}_2 \boldsymbol{\mu}^{(i)}_2 + \cdots + n^{(i)}_M \boldsymbol{\mu}^{(i)}_M),\\
\boldsymbol{\sigma}^2 =& \frac{\sum_{j=1}^{M} n^{(i)} (\boldsymbol{\sigma}^i_j)^2 + \sum_{j=1}^{M} n_j^i (\boldsymbol{\mu}_j^i - \boldsymbol{\mu}^i)^2}{\sum_{j=1}^{M} n_j^i}.
\end{split}
\end{equation}
\]

主要实验




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

work-life balance.

ansible干啥用的就不多介绍了,这篇文章主要在说ansible的安装、开局配置、免密登录。

ansible安装

  1. 查看系统版本
cat /etc/openEuler-latest

输出内容如下:

openeulerversion=openEuler-24.03-LTS

compiletime=2024-05-27-21-31-28

gccversion=12.3.1-30.oe2403

kernelversion=6.6.0-28.0.0.34.oe2403

openjdkversion=1.8.0.412.b08-5.oe2403

  1. 清除软件库缓存
dnf clean all
  1. 建议软件库缓存
dnf makecache 
  1. 安装epel-release软件仓
    1. 下载对应版本epel-release的软件仓库
# 不同系统版本需要安装不同的epel-release
wget https://mirrors.aliyun.com/repo/epel-testing.repo
2. 重新建立软件库索引
mv epel-testing.repo /etc/yum.repo.d/
dnf clean all 
dnf makecache 

  1. 安装ansible
dnf -y install ansible

等待安装完成即可

开局配置

  1. 常用文件介绍

/etc/ansible/hosts ## 用于存放需要批量管理的主机IP或主机名称

/etc/ansible/ansible.cfg ## 该文件为ansible的主要配置文件

  1. 添加主机到ansible
192.168.0.10    ansible_ssh_pass=主机密码        ansible_ssh_user=主机账号
192.168.0.11    ansible_ssh_pass=主机密码        ansible_ssh_user=主机账号
192.168.0.12    ansible_ssh_pass=主机密码       ansible_ssh_user=主机账号

ansible_ssh_pass:远程主机登录密码

ansible_ssh_user:远程主机登录账号

  1. 远程执行ping命令,会发现执行报错
ansible all -m ping

输出内容如下:

192.168.0.10 | FAILED! => {

"msg": "Using a SSH password instead of a key is not possible because Host Key checking is enabled and sshpass does not support
this. Please add this host's fingerprint to your known_hosts file to
manage this host."

}

192.168.0.11 | FAILED! => {

"msg": "Using a SSH password instead of a key is not possible because Host Key checking is enabled and sshpass does not support
this. Please add this host's fingerprint to your known_hosts file to
manage this host."

}

192.168.0.12 | FAILED! => {

"msg": "Using a SSH password instead of a key is not possible because Host Key checking is enabled and sshpass does not support
this. Please add this host's fingerprint to your known_hosts file to
manage this host."

}

出现这个问题主要是因为ansible默认是没有开启账号密码登录的,默认采用证书登录,只需要在配置文件中把证书登录关闭就可以执行成功了。

进入/etc/ansible/ansible.cfg文件,将host_key_checking = False取消注释或者增加该内容即可

再次重新执行就不会有问题了,成功后输出内容如下

192.168.0.11 | SUCCESS => {

"ansible_facts": {

    "discovered_interpreter_python": "/usr/bin/python"

},

"changed": false,

"ping": "pong"

}

192.168.0.10 | SUCCESS => {

"ansible_facts": {

    "discovered_interpreter_python": "/usr/bin/python"

},

"changed": false,

"ping": "pong"

}

192.168.0.12 | SUCCESS => {

"ansible_facts": {

    "discovered_interpreter_python": "/usr/bin/python"

},

"changed": false,

"ping": "pong"

}

配置免密登录

  1. 生成密钥
ssh-keygen

一路回车即可,输出内容如下:

Generating public/private rsa key pair.

Enter file in which to save the key (/root/.ssh/id_rsa):

Enter passphrase (empty for no passphrase):

Enter same passphrase again:

Your identification has been saved in /root/.ssh/id_rsa

Your public key has been saved in /root/.ssh/id_rsa.pub

The key fingerprint is:

SHA256:+RGyyNnrIHOLllk+e2hpNyTmxjBZkMY5vvDmTGuEh5g root@ecs-5352

The key's randomart image is:

+---[RSA 3072]----+

| . o |

| B |

| o o . . |

| . ...+ + . |

| o = ++ S . |

|E o @ + .o . |

| Bo%o=. . |

| O=@++ |

| o.+o=.. |

+----[SHA256]-----+

  1. 编写playbook脚本文件
- hosts: # 主机组
  remote_user: # 用户名
  tasks:
    - name: push ansible key
      authorized_key: user=root key="{{ lookup('file' ,'密钥存放位置')}}" state=present

示例:

- hosts: all
  remote_user: root
  tasks:
    - name: push ansible key
      authorized_key: user=root key="{{ lookup('file' ,'/root/.ssh/id_rsa.pub')}}" state=present
  1. 执行playbook脚本文件
ansible-playbook push_key.yml

输出结果如下表示执行成功:

[root@ecs-5352 yml]# ansible-playbook push_key.yml

PLAY [all]


TASK [Gathering Facts]


ok: [192.168.0.10]

ok: [192.168.0.12]

ok: [192.168.0.11]

TASK [push ansible key]


changed: [192.168.0.10]

changed: [192.168.0.12]

changed: [192.168.0.11]

PLAY RECAP


192.168.0.10 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

192.168.0.11 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

192.168.0.12 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

  1. 测试是否可以免密
    1. 将ansible.cfg配置文件中的host_key_checking = False注释掉

2. 删除hosts文件主机后面的用户名和密码

3. 测试执行ping命令
ansible all -m ping

输出结果如下:

192.168.0.10 | SUCCESS => {

"ansible_facts": {

    "discovered_interpreter_python": "/usr/bin/python"

},

"changed": false,

"ping": "pong"

}

192.168.0.12 | SUCCESS => {

"ansible_facts": {

    "discovered_interpreter_python": "/usr/bin/python"

},

"changed": false,

"ping": "pong"

}

192.168.0.11 | SUCCESS => {

"ansible_facts": {

    "discovered_interpreter_python": "/usr/bin/python"

},

"changed": false,

"ping": "pong"

}

  1. 再次测试

直接在ansible主机上,使用ssh命令测试是否可以免密登录

ssh root@192.168.0.11

无需输入密码即可登录成功

argoworkflow-8-workflow-notify.png

本篇介绍一下 ArgoWorkflow 中的 ExitHandler 和 LifecycleHook 功能,可以根据流水线每一步的不同状态,执行不同操作,一般用于发送通知。

1. 概述

本篇介绍一下 ArgoWorkflow 中的 ExitHandler 和 LifecycleHook 功能,可以根据流水线每一步的不同状态,执行不同操作,一般用于发送通知。

比如当某个步骤,或者某个 Workflow 执行失败时,发送邮件通知。

在 ArgoWorkflow 不同版本中中有两种实现方式:

  • 1)v2.7 版本开始提供了 exit handler 功能,可以指定一个在流水线运行完成后执行的模板。同时这个模板中还可以使用 when 字段来做条件配置,以实现比根据当前流水线运行结果来执行不同流程。
    • 已废弃,v3.3 版本后不推荐使用
  • 2)v.3.3 版本新增 LifecycleHook,exit handler 功能则不推荐使用了,LifecycleHook 提供了更细粒度以及更多功能,exit handler 可以看做是一个简单的 LifecycleHook。

2. ExitHandler

虽然官方已经不推荐使用该功能了,但是还是简单介绍一下。

ArgoWorkflow 提供了 spec.onExit 字段,可以指定一个 template,当 workflow 执行后(不论成功或者失败)就会运行 onExit 指定的 template。

类似于 Tekton 中的 finally 字段

同时这个 template 中可以使用 when 字段来做条件配置。比如根据当前流水线运行结果来执行不同流程。

比如下面这个 Demo,完整 Workflow 内容如下:

# An exit handler is a template reference that executes at the end of the workflow
# irrespective of the success, failure, or error of the primary workflow. To specify
# an exit handler, reference the name of a template in 'spec.onExit'.
# Some common use cases of exit handlers are:
# - sending notifications of workflow status (e.g. e-mail/slack)
# - posting the pass/fail status to a webhook result (e.g. github build result)
# - cleaning up workflow artifacts
# - resubmitting or submitting another workflow
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: exit-handlers-
spec:
  entrypoint: intentional-fail
  onExit: exit-handler
  templates:
    # primary workflow template
    - name: intentional-fail
      container:
        image: alpine:latest
        command: [sh, -c]
        args: ["echo intentional failure; exit 1"]

    # exit handler related templates
    # After the completion of the entrypoint template, the status of the
    # workflow is made available in the global variable {{workflow.status}}.
    # {{workflow.status}} will be one of: Succeeded, Failed, Error
    - name: exit-handler
      steps:
        - - name: notify
            template: send-email
          - name: celebrate
            template: celebrate
            when: "{{workflow.status}} == Succeeded"
          - name: cry
            template: cry
            when: "{{workflow.status}} != Succeeded"
    - name: send-email
      container:
        image: alpine:latest
        command: [sh, -c]
        # Tip: {{workflow.failures}} is a JSON list. If you're using bash to read it, we recommend using jq to manipulate
        # it. For example:
        #
        # echo "{{workflow.failures}}" | jq -r '.[] | "Failed Step: \(.displayName)\tMessage: \(.message)"'
        #
        # Will print a list of all the failed steps and their messages. For more info look up the jq docs.
        # Note: jq is not installed by default on the "alpine:latest" image, however it can be installed with "apk add jq"
        args: ["echo send e-mail: {{workflow.name}} {{workflow.status}} {{workflow.duration}}. Failed steps {{workflow.failures}}"]
    - name: celebrate
      container:
        image: alpine:latest
        command: [sh, -c]
        args: ["echo hooray!"]
    - name: cry
      container:
        image: alpine:latest
        command: [sh, -c]
        args: ["echo boohoo!"]

首先是通过 spec.onExit 字段配置了一个 template

spec:
  entrypoint: intentional-fail
  onExit: exit-handler

这个 template 内容如下:

    - name: exit-handler
      steps:
        - - name: notify
            template: send-email
          - name: celebrate
            template: celebrate
            when: "{{workflow.status}} == Succeeded"
          - name: cry
            template: cry
            when: "{{workflow.status}} != Succeeded"

内部包含 3 个步骤,每个步骤又是一个 template:

  • 1)发送邮件,无论成功或者失败
  • 2)若成功则执行 celebrate
  • 3)若失败则执行 cry

该 Workflow 不论执行结果如何,都会发送邮件,邮件内容包含了任务的执行信息,若是执行成功则会额外打印执行成功,若是执行失败则会打印执行失败。

为了简单,这里所有操作都使用 echo 命令进行模拟

由于在主 template 中最后执行的是
exit 1
命令,因此会判断为执行失败,会发送邮件并打印失败信息,Pod 列表如下:

[root@argo-1 lifecyclehook]# k get po
NAME                                              READY   STATUS      RESTARTS        AGE
exit-handlers-44ltf                               0/2     Error       0               2m45s
exit-handlers-44ltf-cry-1621717811                0/2     Completed   0               2m15s
exit-handlers-44ltf-send-email-2605424148         0/2     Completed   0               2m15s

各个 Pod 日志

[root@argo-1 lifecyclehook]# k logs -f exit-handlers-44ltf-cry-1621717811
boohoo!
time="2024-05-25T11:34:39.472Z" level=info msg="sub-process exited" argo=true error="<nil>"
[root@argo-1 lifecyclehook]# k logs -f exit-handlers-44ltf-send-email-2605424148
send e-mail: exit-handlers-44ltf Failed 30.435347. Failed steps [{"displayName":"exit-handlers-44ltf","message":"Error (exit code 1)","templateName":"intentional-fail","phase":"Failed","podName":"exit-handlers-44ltf","finishedAt":"2024-05-25T11:34:16Z"}]
time="2024-05-25T11:34:44.424Z" level=info msg="sub-process exited" argo=true error="<nil>"
[root@argo-1 lifecyclehook]# k logs -f exit-handlers-44ltf
intentional failure
time="2024-05-25T11:34:15.856Z" level=info msg="sub-process exited" argo=true error="<nil>"
Error: exit status 1

至此,这个 exitHandler 功能就可以满足我们基本的通知需求了,比如将结果以邮件发出,或者对接外部系统 Webhook,更加复杂的需求也可以实现。

不过存在一个问题,就是 exitHandler 是 Workflow 级别的,只能整个 Workflow 执行完成才会执行 exitHandler。

如果想要更细粒度的,比如 template 级别则做不到,v3.3 中提供的 LifecycleHook 则实现了更加细粒度的通知。

3. LifecycleHook

LifecycleHook 可以看做是一个比较灵活的 exit hander,官方描述如下:

Put differently, an exit handler is like a workflow-level
LifecycleHook
with an expression of
workflow.status == "Succeeded"
or
workflow.status == "Failed"
or
workflow.status == "Error"
.

LifecycleHook 有两种级别:

  • Workflow 级别
  • template 级别

Workflow 级别

Workflow 级别的 LifecycleHook 和 exitHandler 基本类似。

下面就是一个 Workflow 级别的 LifecycleHook Demo,完整 Workflow 内容如下:

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: lifecycle-hook-
spec:
  entrypoint: main
  hooks:
    exit: # Exit handler
      template: http
    running:
      expression: workflow.status == "Running"
      template: http
  templates:
    - name: main
      steps:
      - - name: step1
          template: heads
    
    - name: heads
      container:
        image: alpine:3.6
        command: [sh, -c]
        args: ["echo \"it was heads\""]
    
    - name: http
      http:
        # url: http://dummy.restapiexample.com/api/v1/employees
        url: "https://raw.githubusercontent.com/argoproj/argo-workflows/4e450e250168e6b4d51a126b784e90b11a0162bc/pkg/apis/workflow/v1alpha1/generated.swagger.json"

首先是配置 hook

spec:
  entrypoint: main
  hooks:
    exit: # Exit handler
      template: http
    running:
      expression: workflow.status == "Running"
      template: http

可以看到,原有的 onExit 被 hooks 字段替代了,同时 hooks 字段支持指定多个 hook,每个 hook 中可以通过 expression 设置不同的条件,只有满足条件时才会执行。

这里的 template 则是一个内置的 http 类型的 template

    - name: http
      http:
        # url: http://dummy.restapiexample.com/api/v1/employees
        url: "https://raw.githubusercontent.com/argoproj/argo-workflows/4e450e250168e6b4d51a126b784e90b11a0162bc/pkg/apis/workflow/v1alpha1/generated.swagger.json"

该 Workflow 的主 template 比较简单,就是使用 echo 命令打印一句话,因此会执行成功,那么 hooks 中的两个 hooks 都会执行。

两个 hook 对应的都是同一个 template,因此会执行两遍。

template 级别

template 级别的 hooks 则是提供了更细粒度的配置,比如可能用户比较关心 Workflow 中某一个步骤的状态,可以单独为该 template 设置 hook。

下面是一个template 级别的 hooks demo,Workflow 完整内容如下:

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: lifecycle-hook-tmpl-level-
spec:
  entrypoint: main
  templates:
    - name: main
      steps:
        - - name: step-1
            hooks:
              running: # Name of hook does not matter
                # Expr will not support `-` on variable name. Variable should wrap with `[]`
                expression: steps["step-1"].status == "Running"
                template: http
              success:
                expression: steps["step-1"].status == "Succeeded"
                template: http
            template: echo
        - - name: step2
            hooks:
              running:
                expression: steps.step2.status == "Running"
                template: http
              success:
                expression: steps.step2.status == "Succeeded"
                template: http
            template: echo

    - name: echo
      container:
        image: alpine:3.6
        command: [sh, -c]
        args: ["echo \"it was heads\""]

    - name: http
      http:
        # url: http://dummy.restapiexample.com/api/v1/employees
        url: "https://raw.githubusercontent.com/argoproj/argo-workflows/4e450e250168e6b4d51a126b784e90b11a0162bc/pkg/apis/workflow/v1alpha1/generated.swagger.json"

内容和 Workflow 级别的 Demo 差不多,只是 hooks 字段的位置不同

spec:
  entrypoint: main
  templates:
    - name: main
      steps:
        - - name: step-1
            hooks:
              # ...
            template: echo
        - - name: step2
            hooks:
						  # ...
            template: echo

在 spec.templates 中我们分别为不同的步骤配置了 hooks,相比与 exiHandler 则更加灵活。

如何替代 exitHandler

LifecycleHook 可以完美替代 Exit Handler,
就是把 Hook 命名为 exit
,虽然 hook 的命名无无关紧要,但是如果是 exit 则是会特殊处理。

官方原文如下:

You must not name a
LifecycleHook
exit
or it becomes an exit handler; otherwise the hook name has no relevance.

这个 exit 直接是写死在代码里的,具体如下:

const (
    ExitLifecycleEvent = "exit"
)

func (lchs LifecycleHooks) GetExitHook() *LifecycleHook {
    hook, ok := lchs[ExitLifecycleEvent]
    if ok {
       return &hook
    }
    return nil
}

func (lchs LifecycleHooks) HasExitHook() bool {
    return lchs.GetExitHook() != nil
}

那么我们只需要将 LifecycleHook 命名为 exit 即可替代 exit handler,就像这样:

apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: lifecycle-hook-
spec:
  entrypoint: main
  hooks:
    exit: # if named exit, it'a an Exit handler
      template: http
  templates:
    - name: main
      steps:
      - - name: step1
          template: heads
    - name: http
      http:
        # url: http://dummy.restapiexample.com/api/v1/employees
        url: "https://raw.githubusercontent.com/argoproj/argo-workflows/4e450e250168e6b4d51a126b784e90b11a0162bc/pkg/apis/workflow/v1alpha1/generated.swagger.json"

4. 常见通知模板

通知一般支持 webhook、email、slack、微信通知等方式。

在 ArgoWorkflow 中则是准备对应的模板即可。

Webhook

这应该是最通用的一种方式,收到消息后具体做什么事情,可以灵活的在 webhook 服务调整。

对于 ArgoWorkflow 模板就是
执行
curl
命令即可
,因此只需要一个包含 curl 工具的容器

apiVersion: argoproj.io/v1alpha1
kind: ClusterWorkflowTemplate
metadata:
  name: step-notify-webhook
spec:
  templates:
    - name: webhook
      inputs:
        parameters:
          - name: POSITIONS # 指定什么时候运行,多个以逗号隔开,例如:Pending,Running,Succeeded,Failed,Error
            value: "Succeeded,Failed,Error"
          - name: WEBHOOK_ENDPOINT
          - name: CURL_VERSION
            default: "8.4.0"

      container:
        image: curlimages/curl:{{inputs.parameters.CURL_VERSION}}
        command: [sh, -cx]
        args: [
          "curl -X POST  -H \"Content-type: application/json\" -d '{
          \"message\": \"{{workflow.name}} {{workflow.status}}\",
          \"workflow\": {
                \"name\": \"{{workflow.name}}\",
                \"namespace\": \"{{workflow.namespace}}\",
                \"uid\": \"{{workflow.uid}}\",
                \"creationTimestamp\": \"{{workflow.creationTimestamp}}\",
                \"status\": \"{{workflow.status}}\"
              }
        }'
        {{inputs.parameters.WEBHOOK_ENDPOINT}}"
        ]

Email

对于邮件方式,这里简单提供一个使用 Python 发送邮件的 Demo。

# use golangcd-lint for lint
apiVersion: argoproj.io/v1alpha1
kind: ClusterWorkflowTemplate
metadata:
  name: step-notify-email
spec:
  templates:
    - name: email
      inputs:
        parameters:
          - name: POSITIONS # 指定什么时候运行,多个以逗号隔开,例如:Pending,Running,Succeeded,Failed,Error
            value: "Succeeded,Failed,Error"
          - name: CREDENTIALS_SECRET
          - name: TO # 收件人邮箱
          - name: PYTHON_VERSION
            default: "3.8-alpine"
      script:
        image: docker.io/library/python:{{inputs.parameters.PYTHON_VERSION}}
        command: [ python ]
        env:
          - name: TO
            value: '{{inputs.parameters.TO}}'
          - name: HOST
            valueFrom:
              secretKeyRef:
                name: '{{inputs.parameters.CREDENTIALS_SECRET}}'
                key: host
          - name: PORT
            valueFrom:
              secretKeyRef:
                name: '{{inputs.parameters.CREDENTIALS_SECRET}}'
                key: port
          - name: FROM
            valueFrom:
              secretKeyRef:
                name: '{{inputs.parameters.CREDENTIALS_SECRET}}'
                key: from
          - name: USERNAME
            valueFrom:
              secretKeyRef:
                name: '{{inputs.parameters.CREDENTIALS_SECRET}}'
                key: username
          - name: PASSWORD
            valueFrom:
              secretKeyRef:
                name: '{{inputs.parameters.CREDENTIALS_SECRET}}'
                key: password
          - name: TLS
            valueFrom:
              secretKeyRef:
                name: '{{inputs.parameters.CREDENTIALS_SECRET}}'
                key: tls
        source: |
          import smtplib
          import ssl
          import os
          from email.header import Header
          from email.mime.text import MIMEText

          smtp_server = os.getenv('HOST')
          port = os.getenv('PORT')
          sender_email = os.getenv('FROM')
          receiver_emails = os.getenv('TO')
          user = os.getenv('USERNAME')
          password = os.getenv('PASSWORD')
          tls = os.getenv('TLS')

          # 邮件正文,文本格式
          # 构建邮件消息
          workflow_info = f"""\
            "workflow": {{
              "name": "{{workflow.name}}",
              "namespace": "{{workflow.namespace}}",
              "uid": "{{workflow.uid}}",
              "creationTimestamp": "{{workflow.creationTimestamp}}",
              "status": "{{workflow.status}}"
            }}
          """
          msg = MIMEText(workflow_info, 'plain', 'utf-8')
          # 邮件头信息
          msg['From'] = Header(sender_email)  # 发送者
          msg['To'] = Header(receiver_emails)  # 接收者
          subject = '{{workflow.name}} {{workflow.status}}'
          msg['Subject'] = Header(subject, 'utf-8')  # 邮件主题
          if tls == 'True':
            context = ssl.create_default_context()
            server = smtplib.SMTP_SSL(smtp_server, port, context=context)
          else:
            server = smtplib.SMTP(smtp_server, port)

          if password != '':
            server.login(user, password)

          for receiver in [item for item in receiver_emails.split(' ') if item]:
            server.sendmail(sender_email, receiver, msg.as_string())

            server.quit()


【ArgoWorkflow 系列】
持续更新中,搜索公众号【
探索云原生
】订阅,阅读更多文章。


5. 小结

本文主要分析了 Argo 中的通知触发机制,包括旧版的 exitHandler 以及新版的 LifecycleHook,并提供了几个简单的通知模板。

最后则是推荐使用更加灵活的 LifecycleHook。

Logisim单周期cpu设计文档与思考题


设计文档

支持指令集

指令 格式 描述(RTL) 机器码 OPCODE/FUNCT
add add rd rs rt GPR[rd] <- GPR[rs]+GPR[rt] R型 0/100000
sub sub rd rs rt GPR[rd] <- GPR[rs]-GPR[rt] R型 0/100010
ori ori rt rs imme GPR[rt] <- GPR[rs] or imme I型 001101
lw lw rt offset(base) GPR[rt] <- memory[GPR[base]+offset] I型 100011
sw sw rt offset(base) memory[GPR[base]+offset] <- GPR[rt] I型 101011
beq beq rs rt offset if(GPR[rs]==GPR[rt]) PC <- PC+4+sign_extend(offset||00) I型 000100
lui lui rt imme GPR[rt] <- imme||0^16 I型 001111
jal jal target PC <- PC31..28||target||00 并且 GPR[31] <- PC + 4 J型 000011
jr jr rs PC <- GPR[rs] R型 0/001000
sll sll rd rt s GPR[rd] <- GPR[rt] << s R型 0/000000
jalr jalr rd rs PC <- GPR[rs] , GPR[rd] <- PC+4 R型 0/001001
lb lb rt offset(rs) GPR[rt] <- memory[GPR[rs]+offset] I型 100000
sb sb rt offset(rs) memory[GPR[rs]+offset] <- GPR[rt] I型 101000
slt slt rd rs rt GPR[rd] <- (GPR[rs] < GPR[rt]) R型 0/101010
nop null 不进行任何操作,空置一周期 null 0x00000000

注意

  • 其中
    sll
    指令为
    R型指令
    ,这是因为我们最多移位31位,没必要用16位的立即数保存。故s为5位,是一种特殊的R型(只有两个寄存器参与操作)
  • nop虽然不需要进行任何操作,但是
    仍然需要进行PC <- PC + 4
    ,所以在设计NPCop时不要忘了2'b00也应该能被nop激活。

mips指令集机器码补充

R型指令

opcode rs rt rd shamt funct
位长 6 5 5 5 5 6
占位 31-26 25-21 20-16 15-11 10-6 5-0

I型指令

opcode rs rt immediate
位长 6 5 5 16
占位 31-26 25-21 20-16 15-0

J型指令

opcode instr_index
位长 6 26
占位 31-26 25-0


数据通路形式建模

模块分析

  • IFU(Instruction Fetch Unit 取指令单元)

模块描述

  • 内部包括 PC(程序计数器)、IM(指令存储器)、NPC(进行指令的转移地址计算)。
  • PC 用寄存器实现,应具有异步复位功能,复位值为起始地址。
  • 起始地址:0x00003000。
  • 地址范围:0x00003000 ~ 0x00006FFF。
  • IM 用 ROM 实现,容量为 4096 × 32bit。
  • ROM 内部的起始地址是从 0 开始的,即 ROM 的 0 位置存储的是 PC 为 0x00003000 的指令,每条指令是一个 32bit 常数。
  • 经过以上分析,不难发现 ROM 实际地址宽度仅需 12 位,请使用恰当的方法将 PC 中储存的地址同 IM 联系起来。

模块功能及引脚定义

  • PC:当Reset=1时,异步复位至地址0x0000_3000
信号 方向 描述
CLK I 1 单周期的时钟信号
Reset I 1 异步复位
DI I 1 NPC传来的新的指令地址
DO O 1 当前指令地址
  • NPC:计算下一条指令的地址
信号 方向 描述
PC I 32 当前指令地址
imm16 I 16 beq时跳转到的地址
ZERO I 1 ALU减法结果是否为 0
NPCop I 2 决定NPC应该进行何种计算
NPC O 32 下一条指令的地址
  • IM:读取指令机器码并分解
信号 方向 描述
PC I 32 当前指令地址
OPCODE O 6 当前指令的OPCODE
rs O 5 rs编号
rt O 5 rt编号
rd O 5 rd编号
imm16 O 16 立即数
FUNCT O 6 当前指令的FUNCT
  • GRF(寄存器堆)

模块描述

  • 用具有写使能的寄存器实现,寄存器总数为 32 个,应具有异步复位功能。
  • 0 号寄存器的值始终保持为 0。其他寄存器初始值(复位后)均为 0,无需专门设置。
  • 0号寄存器采用接地设计,故实际上只有31个寄存器。

模块功能及引脚定义

  • GRF:寄存器堆用于存储寄存器中的数据
信号 方向 描述
CLK I 1 时钟信号
Reset I 1 异步复位信号
GRFWe I 1 写使能信号
A1 I 5 rs寄存器编号
A2 I 5 rt寄存器编号
A3 I 5 rd寄存器编号
WD I 32 写入rd的数据
RD1 O 32 rs寄存器读出值
RD2 O 32 rt寄存器读出值
  • ALU(算术逻辑单元)

模块描述

  • 提供 32 位加、减、或运算及大小比较功能。
  • 加减法按无符号处理(不考虑溢出)。

模块功能及引脚定义

信号 方向 描述
A I 32 GPR[rs]
B I 32 GPR[rt] 或 扩展后的立即数
ALUop I 2 执行何种运算
RD O 32 运算结果
ZERO O 1 执行减法时结果是否为 0
  • 注:规定ALUop的2'b00,2'b01,2'b10分别表示add、sub、ori操作
  • DM(数据存储器)

模块描述

  • 使用 RAM 实现,容量为 3072 × 32bit,应具有异步复位功能,复位值为 0x00000000。
  • 起始地址:0x00000000。
  • 地址范围:0x00000000 ~ 0x00002FFF。
  • RAM 应使用双端口模式,即设置 RAM 的 Data Interface 属性为 Separate load and store ports。

模块功能及引脚定义

信号 方向 描述
A I 32 数据存储器地址
DI I 32 写入的数据
We I 1 写使能
CLK I 1 时钟信号,异步复位
Reset I 1 复位信号
DO O 32 输出的数据
  • EXT(扩展单元)

模块描述

  • 使用 Logisim 内置的 Bit Extender。
  • 作用为将immediate16拓展为32bit。
  • 具有拓展选择信号EXTop。

控制器(Controller)建模

  • 模块描述
  • 使用与或门阵列构造控制信号。
  • 分析可知,决定控制信号的就是当前在执行哪种指令,我们便需要建立每种信号的布尔表达式
  • 与阵列中,我们将每种指令作为输出,布尔表达式建立方法如:beq的opcode为000100,则beq=!op[5]
    !op[4]
    !op[3]
    op[2]
    !op[1]*!op[0]
  • 或阵列中,只需要把使得控制信号为真的指令或起来即可。
  • 模块功能及引脚定义
信号 方向 描述 多位控制信号的具体描述
OPCODE I 6 指令的opcode
FUNCT I 6 指令的funct
NPCop O 2 决定NPC应该进行何种计算 2'b00 : +4
2'b01 : beq
2'b10 : jal
2'b11 : jr
WRSel O 2 GRF写入地址来源 2'b00 : rt
2'b01 : rd
2'b10 : ra(jal)
WDSel O 2 GRF写入数据来源 2'b00 : ALU的输出
2'b01 : DM的输出
2'b10 : 立即数
2'b11 : PC+4(jal)
EXTop O 2 如何扩展立即数 2'b00 : 无符号扩展
2'b01 : 有符号扩展
2'b10 : 在低位扩展16个0
GRFWe O 1 GRF写使能
ALUop O 2 进行何种运算 2'b00 : 加法
2'b01 : 减法
2'b10 : 或运算
2'b11 : 左移运算
BSel O 1 运算数是否为立即数
DMWe O 1 DM写使能


思考题

  1. 上面我们介绍了通过 FSM 理解单周期 CPU 的基本方法。请大家指出单周期 CPU 所用到的模块中,哪些发挥状态存储功能,哪些发挥状态转移功能。


    :单周期cpu涉及到的模块有:NPC、PC、IM、GRF、ALU、DM,可以将其划分为两个FSM:分别为NPC、PC、IM构成的Moore型FSM,称之为IFU 和 GRF、ALU、DM构成的下游mealy型FSM。在IFU中,PC发挥状态存储功能,NPC发挥状态转移功能;在下游FSM中,GRF发挥状态存储功能,整个IFU发挥状态转移功能。



  2. 现在我们的模块中 IM 使用 ROM, DM 使用 RAM, GRF 使用 Register,这种做法合理吗? 请给出分析,若有改进意见也请一并给出。

    :我认为合理。ROM是只读存储器,只能读取而无法写入,在程序运行的过程中,我们已经将mips翻译为机器码导入IM的ROM中,无须修改,故合理。DM是cpu的主存,会与ALU和GRF间交互数据,所以DM需要同时具有读与写的功能,使用RAM合理。GRF本来就是register file,临时的存储变量,符合寄存器的特性,合理。



  3. 在上述提示的模块之外,你是否在实际实现时设计了其他的模块?如果是的话,请给出介绍和设计的思路。

    :无。



  4. 事实上,实现 nop 空指令,我们并不需要将它加入控制信号真值表,为什么?

    :由于我们的controller是通过与或阵列实现的,nop的机器码为0x0000_0000,在不考虑nop时,controller的与阵列的所有已经设计好的指令输出都为低电平,即cpu不会有任何高电平写使能、NPC也按照正常方式计算一下条指令地址,此时电路表现为空置一周期,故不需要考虑nop指令的设计。



  5. 阅读 Pre 的 “MIPS 指令集及汇编语言” 一节中给出的测试样例,评价其强度(可从各个指令的覆盖情况,单一指令各种行为的覆盖情况等方面分析),并指出具体的不足之处。

    :我认为强度不够。在指令集覆盖方面缺少对sub指令的测试,同时在具体单种指令的测试上,ori的数据缺少边界情况的测试如立即数为0x0或0xffff;add指令缺少运算数包含0x0的情况;sw指令测试存在问题:lw的偏移量是负数的情况没有测试;lw与sw是同样的问题;beq指令的问题是缺少寄存器是负数的判断和跳转。

    附上成功自测出GRFbug的改进数据。
     # add sub ori lw sw beq lui nop
     ori $a0 $0 123#a0 = 123
     ori $a1 $a0 456#a1 = 507
     ori $a1 $a1 0#a1 = 507
     ori $a2 0xffff#a2 = 0xffff
    
     lui $a3 123#a3 = 0x007B_0000
     lui $t0 0xffff
     ori $t0 $t0 0xffff#t0 = 0xffffffff (-1)
    
     # wrong from here s0 is -1(wrong)
     add $s0 $a0 $a2#s0 = 65658
     add $s1 $a0 $t0#s1 = 122
     add $s2 $t0 $t0#s2 = -2
     add $s3 $0 $t0#s3 = -1
    
     sub $s4 $a0 $a1#s4 = -384
     sub $s5 $a1 $a0#s5 = 384
     sub $s6 $t0 $t0#s6 = 0
     sub $s7 $a0 $t0#s7 = 124
    
    
     ori $t1 0x0000#t1 = 0
     sw $a0 0($t1)#123
     sw $a2 4($t1)#0xffff
     sw $0 8($t1)#0
     sw $t0 12($t1)#-1
     ori $t2 20#t2 = 20
     sw $a1 -4($t2)#507
    
     ori $t3 $0 4
     lw $t4 4($t3)#t4 = 0xffff
     lw $t5 8($t3)#t5 = 0
     lw $t6 12($t3)#t6 = -1
     lw $t7 -4($t3)#t7 = 507
    
     beq $a0 $a1 loop1#unequal
     beq $s3 $t0 loop2#equal
    
     loop1:lw $t8 0($t1)
     loop2:lw $t8 4($t1)#t8 = 0xffff
    


前一篇:《全面解释人工智能LLM模型的真实工作原理(三)》

序言: 本节作为整篇的收官之作,自然少不了与当今最先进的AI模型相呼应。这里我们将简单介绍全球首家推动人工智能生成人类语言的公司——OpenAI的GPT模型的基本原理。如果你也希望为人类的发展做出贡献,并投身于AI行业,这无疑是一个绝佳的起点。其他知识都是进入该行业的基础,而理解该模型是必须的。OpenAI的创始团队中包括科技巨头Elon Musk,以及2024年诺贝尔奖得主Geoffrey Hinton的学生伊利亚·苏茨克弗(Ilya Sutskever)。他们都是全球有钱又最具智慧和前瞻性的人物代表。OpenAI最初公开了ChatGPT-2的语言模型(LLM)源代码,但在随后的ChatGPT-3及之后的版本中停止了开源,逐渐背离了最初的开放承诺,导致公司内部核心成员的相继离开。本节介绍的模型由OpenAI参与者、现任斯坦福大学教授李飞飞的学生Andrej Karpathy基于ChatGPT3模型而来。

(关注不迷路,及时收到最新的人工智能资料更新)

GPT架构

接下来谈谈GPT架构。大多数GPT模型(尽管有不同的变化)都使用这种架构。如果你跟着文章读到这里,这部分应该相对容易理解。使用框图表示法,这就是GPT架构的高级示意图:

此时,除了“GPT Transformer块”,其他模块我们都已详细讨论过。这里的+号只是表示两个向量相加(这意味着两个嵌入必须同样大小)。来看一下这个GPT Transformer块:

就是这样。之所以称之为“Transformer”,是因为它源自并属于一种Transformer架构——我们将在下一节中详细了解。理解上没有影响,因为这里展示的所有模块我们都已讨论过。让我们回顾一下到目前为止构建这个GPT架构的过程:

• 我们了解到神经网络接收数字并输出其他数字,权重是可训练的参数

• 我们可以对这些输入/输出数字进行解释,赋予神经网络现实世界的意义

• 我们可以串联神经网络创建更大的网络,并可以将每一个称为“块”,用框来表示以简化图解。每个块的作用都是接收一组数字并输出另一组数字

• 我们学习了很多不同类型的块,每种块都有其不同的作用

• GPT只是这些块的一个特殊排列,如上图所示,解释方式在第一部分已讨论过

随着时间的推移,人们在此基础上做出了各种修改,使得现代LLM更加强大,但基本原理保持不变。

现在,这个GPT Transformer实际上在原始Transformer论文中被称为“解码器”。让我们看看这一点。

Transformer架构

这是驱动语言模型能力迅速提升的关键创新之一。Transformer不仅提高了预测准确性,还比先前的模型更高效(更容易训练),允许构建更大的模型。这是GPT架构的基础。

观察GPT架构,你会发现它非常适合生成序列中的下一个词。它基本遵循我们在第一部分讨论的逻辑:从几个词开始,然后逐个生成词。但是,如果你想进行翻译呢?比如,你有一句德语句子(例如“Wo wohnst du?” = “Where do you live?”),你希望将其翻译成英语。我们该如何训练模型来完成这项任务?

第一步,我们需要找到一种输入德语单词的方法,这意味着我们要扩展嵌入,包含德语和英语。我猜一种简单的输入方式是将德语句子和生成的英文句子连接起来,并将其输入上下文。为了让模型更容易理解,我们可以添加一个分隔符。每一步看起来像这样:

这可以工作,但仍有改进空间:

• 如果上下文长度固定,有时会丢失原始句子

• 模型需要学习很多内容。包括两种语言,还需要知道
是分隔符,它应该在此处开始翻译

• 每次生成一个词时,都需要处理整个德语句子,存在不同偏移。这意味着相同内容的内部表示不同,模型应该能够通过这些表示进行翻译

Transformer最初就是为此任务创建的,它由“编码器”和“解码器”组成——基本上是两个独立的模块。一个模块仅处理德语句子,生成中间表示(仍然是数值集合)——这被称为编码器。第二个模块生成单词(我们已经见过很多)。唯一的区别是,除了将已生成的单词输入解码器外,还将编码器输出的德语句子作为额外输入。也就是说,在生成语言时,它的上下文是已生成的所有单词加上德语句子。这个模块被称为解码器。

这些编码器和解码器由一些块组成,尤其是夹在其他层之间的注意力块。我们来看“Attention is all you need”论文中的Transformer架构示意图并尝试理解它:

左侧的竖直块集合称为“编码器”,右侧的称为“解码器”。让我们逐个理解每个部分:

前馈网络:前馈网络是没有循环的网络。第一部分中讨论的原始网络就是一个前馈网络。事实上,这个块采用了非常相似的结构。它包含两个线性层,每个层之后都有一个ReLU(见第一部分关于ReLU的介绍)和一个Dropout层。请记住,这个前馈网络适用于每个位置独立。也就是说,位置0有一个前馈网络,位置1有一个,依此类推。但是位置x的神经元不会与位置y的前馈网络相连。这样做的重要性在于防止网络在训练时“偷看”前方信息。

交叉注意力:你会注意到解码器有一个多头注意力,其箭头来自编码器。这里发生了什么?记得自注意力和多头注意力中的value、key、query吗?它们都来自同一个序列。事实上,query只是序列的最后一个词。那么,如果我们保留query,但将value和key来自一个完全不同的序列会怎样?这就是这里发生的情况。value和key来自编码器的输出。数学上没有任何改变,只是key和value的输入来源发生了变化。

Nx:Nx表示这个块重复N次。基本上,你在将一个块层层堆叠,前一个块的输出作为下一个块的输入。这样可以使神经网络更深。从图上看,编码器输出如何传递给解码器可能让人困惑。假设N=5。我们是否将每层编码器输出传递给对应的解码器层?不是的。实际上你只需运行一次编码器,然后将同一表示提供给5个解码器层。

加与归一化块:这与下方相同(作者似乎只是为了节省空间)。

其他内容我们已经讨论过。现在你已经完整理解了Transformer架构,从简单的加法和乘法操作一步步构建到现在的完整自包含解释!你知道如何从头构建Transformer的每一行、每一加法、每一块和每个单词的意义。如果你感兴趣,可以参看这个开源库(开源GPT:
https://github.com/karpathy/nanoGPT),它从头实现了上述的GPT架构。

附录

矩阵乘法

在嵌入部分中,我们引入了向量和矩阵的概念。矩阵有两个维度(行数和列数)。向量也可以看作一个只有一个维度的矩阵。两个矩阵的乘积定义为:

点表示相乘。现在我们再看一下第一张图中蓝色和有机神经元的计算。如果我们将权重写成矩阵,输入作为向量,可以将整个运算表示如下:

如果权重矩阵称为“W”,输入称为“x”,则Wx为结果(在此情况下是中间层)。我们也可以将两者转置写作xW——这是个人偏好的问题。

标准差

在层归一化部分,我们使用了标准差的概念。标准差是一个统计量,用于描述数值的分布范围(在一组数字中),例如,如果所有值都相同,则标准差为零。如果每个值都与这些值的均值相距很远,则标准差会很高。计算一组数字a1, a2, a3…(假设有N个数字)的标准差的公式如下:将每个数字减去均值,然后将每个N个数字的结果平方。将所有这些数字相加,然后除以N,最后对结果开平方根。

位置编码

我们在上文中提到过位置嵌入。位置编码与嵌入向量长度相同,不同之处在于它不是嵌入,且无需训练。我们为每个位置分配一个独特的向量。例如,位置1是一个向量,位置2是另一个,以此类推。

(完结)

欢迎大家在评论区沟通讨论,作者同样可以为您解释模型当中的全部原理和实现过程与细节。