技术

下一个平台Agent 激发LLM涌现——提示工程 LLM微调理论及实践 大佬沉思 LLM外挂知识库 LLMOps 多模态LLM Python一些比较有意思的库 LLM部分技术源码学习 LangChain源码学习 通用分布式计算引擎Ray Python并发 go依赖注入 go collection gc的基本原理 golang性能分析及优化 数据湖 高性能计算与存储 Linux2.1.13网络源代码学习 《大数据经典论文解读》 三驾马车学习 Spark 内存管理及调优 Yarn学习 从Spark部署模式开始讲源码分析 容器狂占内存资源怎么办? 多角度理解一致性 golang io使用及优化模式 Flink学习 c++学习 学习ebpf go设计哲学 ceph学习 学习mesh kvm虚拟化 学习MQ go编译器以及defer实现 学习go 为什么要有堆栈 汇编语言 计算机组成原理 运行时和库 Prometheus client mysql 事务 mysql 事务的隔离级别 mysql 索引 坏味道 学习分布式 学习网络 学习Linux go堆内存分配 golang 系统调用与阻塞处理 Goroutine 调度过程 重新认识cpu mosn有的没的 负载均衡泛谈 单元测试的新解读 《Redis核心技术与实现》笔记 《Prometheus监控实战》笔记 Prometheus 告警学习 calico源码分析 对容器云平台的理解 Prometheus 源码分析 并发的成本 基础设施优化 hashicorp raft源码学习 docker 架构 mosn细节 与微服务框架整合 Java动态代理 编程范式 并发通信模型 《网络是怎样连接的》笔记 go channel codereview gc分析 jvm 线程实现 go打包机制 go interface及反射 如何学习Kubernetes 《编译原理之美》笔记——后端部分 《编译原理之美》笔记——前端部分 Pilot MCP协议分析 go gc 内存管理玩法汇总 软件机制 istio流量管理 Pilot源码分析 golang io 学习Spring mosn源码浅析 MOSN简介 《datacenter as a computer》笔记 学习JVM Tomcat源码分析 Linux可观测性 学习存储 学计算 Gotty源码分析 kubernetes operator kaggle泰坦尼克问题实践 kubernetes扩缩容 神经网络模型优化 直觉上理解深度学习 如何学习机器学习 TIDB源码分析 什么是云原生 Alibaba Java诊断工具Arthas TIDB存储——TIKV 《Apache Kafka源码分析》——简介 netty中的线程池 guava cache 源码分析 Springboot 启动过程分析 Spring 创建Bean的年代变迁 Linux内存管理 自定义CNI IPAM 共识算法 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 docker和k8s安全访问机制 jvm crash分析 Prometheus 学习 Kubernetes监控 容器日志采集 Kubernetes 控制器模型 容器狂占资源怎么办? Kubernetes资源调度——scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes类型系统 源码分析体会 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 jib源码分析之细节 线程排队 跨主机容器通信 jib源码分析及应用 为容器选择一个合适的entrypoint kubernetes yaml配置 《持续交付36讲》笔记 mybatis学习 程序猿应该知道的 无锁数据结构和算法 CNI——容器网络是如何打通的 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 精简代码的利器——lombok 学习 《深入剖析kubernetes》笔记 编程语言那些事儿 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 如何分发计算 Storm 学习 AQS1——论文学习 Unsafe Spark Stream 学习 linux vfs轮廓 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 hbase 泛谈 forkjoin 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 bgp初识 calico学习 AQS——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 《Elasticsearch权威指南》笔记 mockito简介及源码分析 2017软件开发小结—— 从做功能到做系统 《Apache Kafka源码分析》——clients dns隐藏的一个坑 《mysql技术内幕》笔记 log4j学习 为什么netty比较难懂? 递归、回溯、动态规划 apollo client源码分析及看待面向对象设计 学习并发 docker运行java项目的常见问题 OpenTSDB 入门 spring事务小结 分布式事务 javascript应用在哪里 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 java内存模型和jvm内存布局 java exception Linux IO学习 netty内存管理 测试环境docker化实践 netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker namespace和cgroup zookeeper三重奏 数据库的一些知识 Spark 泛谈 链式处理的那些套路 netty回顾 Thrift基本原理与实践(二) Thrift基本原理与实践(一) 回调 异步执行抽象——Executor与Future Docker0.1.0源码分析 java gc Jedis源码分析 深度学习泛谈 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 向Hadoop学习NIO的使用 以新的角度看数据结构 并发控制相关的硬件与内核支持 systemd 简介 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM执行 git maven/ant/gradle/make使用 再看tcp kv系统 java nio的多线程扩展 《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go常用的一些库 Python初学 Goroutine 调度模型 虚拟网络 《程序员的自我修养》小结 Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件 Go基础 JVM类加载 硬币和扑克牌问题 LRU实现 virtualbox 使用 ThreadLocal小结 docker快速入门

架构

大模型推理服务框架 模型服务化(未完成) 大模型RHLF 大模型训练 大模型推理 从Attention到Transformer k8s设备管理 LLM工具栈 ddd从理念到代码 如何应用LLM 小鼠如何驾驭大象(LLM)? 多类型负载协调员Koordinator controller-runtime细节分析 finops学习 kubevela多集群 kubevela中cue的应用 基于k8s的工作流 容器和CPU那些事儿 kubevela源码分析 数据集管理fluid 应用管理平台kubevela karmada支持crd 多集群管理 AutoML和AutoDL 特征平台 实时训练 分布式链路追踪 helm tensorflow原理——python层分析 如何学习tensorflow 数据并行——allreduce 数据并行——ps 机器学习中的python调用c 机器学习训练框架概述 embedding的原理及实践 tensornet源码分析 大模型训练和推理 X的生成——特征工程 tvm tensorflow原理——core层分析 模型演变 《深度学习推荐系统实战》笔记 keras 和 Estimator tensorflow分布式训练 分布式训练的一些问题 基于Volcano的弹性训练 图神经网络 pytorch弹性分布式训练 从混部到统一调度 从RNN到Attention pytorch分布式训练 CNN 《动手学深度学习》笔记 pytorch与线性回归 多活 volcano特性源码分析 推理服务 kubebuilder 学习 mpi 学习pytorch client-go学习 tensorflow学习 提高gpu 利用率 GPU与容器的结合 GPU入门 AI云平台梳理 tf-operator源码分析 k8s批处理调度/Job调度 喜马拉雅容器化实践 Kubernetes 实践 学习rpc BFF openkruise学习 可观察性和监控系统 基于Kubernetes选主及应用 《许式伟的架构课》笔记 Admission Controller 与 Admission Webhook 发布平台系统设计 k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 深入controller openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go informer源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? 《软件设计之美》笔记 mecha 架构学习 Kubernetes events学习及应用 CRI 资源调度泛谈 业务系统设计原则 grpc学习 元编程 以应用为中心 istio学习 下一代微服务Service Mesh 《实现领域驱动设计》笔记 概率论 serverless 泛谈 《架构整洁之道》笔记 处理复杂性 那些年追过的并发 服务器端编程 网络通信协议 架构大杂烩 如何学习架构 《反应式设计模式》笔记 项目的演化特点 反应式架构摸索 函数式编程的设计模式 服务化 ddd反模式——CRUD的败笔 研发效能平台 重新看面向对象设计 业务系统设计的一些体会 函数式编程 《左耳听风》笔记 业务程序猿眼中的微服务管理 DDD实践——CQRS 项目隔离——案例研究 《编程的本质》笔记 系统故障排查汇总及教训 平台支持类系统的几个点 代码腾挪的艺术 abtest 系统设计汇总 《从0开始学架构》笔记 初级权限系统设计 领域驱动理念 现有上传协议分析 移动网络下的文件上传要注意的几个问题 推送系统的几个基本问题 做配置中心要想好的几个基本问题 不同层面的异步 分层那些事儿 性能问题分析 用户认证问题 资源的分配与回收——池 消息/任务队列

标签

k8s设备管理 多类型负载协调员Koordinator controller-runtime细节分析 finops学习 kubevela多集群 kubevela中cue的应用 基于k8s的工作流 容器和CPU那些事儿 kubevela源码分析 数据集管理fluid 应用管理平台kubevela karmada支持crd 多集群管理 helm 从混部到统一调度 volcano特性源码分析 kubebuilder 学习 client-go学习 tf-operator源码分析 k8s批处理调度/Job调度 喜马拉雅容器化实践 Kubernetes 实践 openkruise学习 基于Kubernetes选主及应用 Admission Controller 与 Admission Webhook k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 深入controller openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go informer源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? Kubernetes events学习及应用 CRI 资源调度泛谈 如何学习Kubernetes 以应用为中心 kubernetes operator kubernetes扩缩容 serverless 泛谈 什么是云原生 自定义CNI IPAM docker和k8s安全访问机制 Kubernetes监控 Kubernetes 控制器模型 Kubernetes资源调度——scheduler Kubernetes类型系统 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 Kubernetes源码分析——从kubectl开始 kubernetes yaml配置 CNI——容器网络是如何打通的 当我在说PaaS时,我在说什么 《深入剖析kubernetes》笔记 Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件

机器学习训练框架概述

2022年03月02日

简介

手把手教你如何自己设计实现一个深度学习框架(附代码实现) 对机器学习在工程上的实现和抽象说的比较透。tinynn 只是一个「玩具」版本的深度学习框架,一个成熟的深度学习框架至少还需要:支持自动求导、高运算效率(静态语言加速、支持 GPU 加速)、提供丰富的算法实现、提供易用的接口和详细的文档等等。

从 0 到 1 实现神经网络(Python) 未读。

手把手教你如何自己设计实现一个深度学习框架 未读

从零构建现代深度学习框架(TinyDL-0.01)

发展脉络

大模型时代,AI框架的机遇和挑战AI框架经历了四个阶段的发展:

  1. 早期主要用于研究比如Theano;
  2. 随着CV的兴起,为了追求极致的性能加速,发展出了基于静态图的AI框架比如TensorFlow,它大大提升了性能和部署效率;
  3. 伴随着NLP技术的快速发展,算法复杂度大幅提升,动态图成了提升易用性的关键,典型的比如PyTorch,它有效提升了编程体验,能够与Python生态更好地融合。
  4. 大模型有两个比较大的特点:
    1. 模型结构收敛到Transformer,而且大部分是decoder only的架构;
    2. 模型的规模大,达到百亿/千亿级别,训练推理成本高,训练时间长。

所谓的“模型”本质上是两个部分的合体:计算图和模型参数——神经网络的本质是一个数学定义下的函数,它自身不是一个可执行的程序。举个简单的例子,假设某个模型的函数实际上就是一个二次函数$f(x)=x^2+2x+1$,那它的“计算图”就是$ax^2+bx+c$(简写,实际要变成图结构),参数就是一个字典:{a:1,b:2,c:1}。所以pytorch保存的模型文件实际上是一个python字典(key是模型层的名称,value是对应参数),你用pytorch载入一个保存好的模型,需要先在代码里import 模型定义,然后再load_state_dict。换句话说,实际发布的“模型”本身需要依附于框架而存在,不是一个可执行文件。

抽象层

组件抽象

神经网络运算主要包含训练 training 和预测 predict (或 inference) 两个阶段,训练的基本流程是:输入数据 -> 网络层前向传播 -> 计算损失 -> 网络层反向传播梯度 -> 更新参数,预测的基本流程是 输入数据 -> 网络层前向传播 -> 输出结果。从运算的角度看,主要可以分为三种类型的计算:

  1. 数据在网络层之间的流动:前向传播和反向传播可以看做是张量 Tensor(多维数组)在网络层之间的流动(前向传播流动的是输入输出,反向传播流动的是梯度),每个网络层会进行一定的运算,然后将结果输入给下一层
  2. 计算损失:衔接前向和反向传播的中间过程,定义了模型的输出与真实值之间的差异,用来后续提供反向传播所需的信息
  3. 参数更新:使用计算得到的梯度对网络参数进行更新的一类计算

基于这个三种类型,我们可以对网络的基本组件做一个抽象

  1. tensor 张量,这个是神经网络中数据的基本单位
  2. layer 网络层,负责接收上一层的输入,进行该层的运算,将结果输出给下一层,由于 tensor 的流动有前向和反向两个方向,因此对于每种类型网络层我们都需要同时实现 forward 和 backward 两种运算
  3. loss 损失,在给定模型预测值与真实值之后,该组件输出损失值以及关于最后一层的梯度(用于梯度回传)
  4. optimizer 优化器,负责使用梯度更新模型的参数 然后我们还需要一些组件把上面这个 4 种基本组件整合到一起,形成一个 pipeline

  5. net 组件负责管理 tensor 在 layers 之间的前向和反向传播,同时能提供获取参数、设置参数、获取梯度的接口
  6. model 组件负责整合所有组件,形成整个 pipeline。即 net 组件进行前向传播 -> losses 组件计算损失和梯度 -> net 组件将梯度反向传播 -> optimizer 组件将梯度更新到参数。PS:一般框架都需要一个对象提供一个操作入口

基本的框架图如下图

组件实现

按照上面的抽象,我们可以写出整个流程代码如下。PS:一个架构设计的典型案例

# define model
net = Net([layer1, layer2, ...])
model = Model(net, loss_fn, optimizer)
# training,将 net、loss、optimizer 一起传给 model,model 实现了 forward、backward 和 apply_grad 三个接口分别对应前向传播、反向传播和参数更新三个功能
pred = model.forward(train_X)
loss, grads = model.backward(pred, train_Y)
model.apply_grad(grads)
# inference
test_pred = model.forward(test_X)

tensor 张量是神经网络中基本的数据单位,我们这里直接使用 numpy.ndarray 类作为 tensor 类的实现。layer需要有提供 forward 和 backward 接口进行对应的运算。同时还应该将该层的参数和梯度记录下来。先实现一个基类如下

class Layer(object):
  def __init__(self, name):
      self.name = name
      self.params, self.grads = None, None
  def forward(self, inputs):
      raise NotImplementedError
  def backward(self, grad):
      raise NotImplementedError

最基础的一种网络层是全连接网络层

class Dense(Layer):
  def __init__(self, num_in, num_out,w_init=XavierUniformInit(),b_init=ZerosInit()):
      super().__init__("Linear")
      self.params = {
          "w": w_init([num_in, num_out]),
          "b": b_init([1, num_out])}
      self.inputs = None
  # forward 方法接收上层的输入 inputs,实现  的运算
  def forward(self, inputs):
      self.inputs = inputs
      return inputs @ self.params["w"] + self.params["b"]
  # backward 的方法接收来自上层的梯度,计算关于参数  和输入的梯度,然后返回关于输入的梯度
  def backward(self, grad):
      self.grads["w"] = self.inputs.T @ grad
      self.grads["b"] = np.sum(grad, axis=0)
      return grad @ self.params["w"].T

激活函数可以看做是一种网络层

class Activation(Layer):
  """Base activation layer"""
  def __init__(self, name):
      super().__init__(name)
      self.inputs = None
  def forward(self, inputs):
      self.inputs = inputs
      return self.func(inputs)
  def backward(self, grad):
      return self.derivative_func(self.inputs) * grad
  def func(self, x):
      raise NotImplementedError
  def derivative_func(self, x):
      raise NotImplementedError

net 类负责管理 tensor 在 layers 之间的前向和反向传播

class Net(object):
  def __init__(self, layers):
      self.layers = layers
  # 按顺序遍历所有层,每层计算的输出作为下一层的输入
  def forward(self, inputs):
      for layer in self.layers:
          inputs = layer.forward(inputs)
      return inputs
  # 逆序遍历所有层,将每层的梯度作为下一层的输入
  def backward(self, grad):
      all_grads = [] # 将每个网络层参数的梯度保存下来返回,后面参数更新需要用到
      for layer in reversed(self.layers):
          grad = layer.backward(grad)
          all_grads.append(layer.grads)
      return all_grads[::-1]

  def get_params_and_grads(self):
      for layer in self.layers:
          yield layer.params, layer.grads
  def get_parameters(self):
      return [layer.params for layer in self.layers]
  def set_parameters(self, params):
      for i, layer in enumerate(self.layers):
          for key in layer.params.keys():
              layer.params[key] = params[i][key]

losses 组件需要做两件事情

class BaseLoss(object):
    # 计算损失值
    def loss(self, predicted, actual):
        raise NotImplementedError
    # 计算损失值和关于预测值的梯度
    def grad(self, predicted, actual):
        raise NotImplementedError

optimizer 主要实现一个接口 compute_step,这个方法根据当前的梯度,计算返回实际优化时每个参数改变的步长。

class BaseOptimizer(object):
    def __init__(self, lr, weight_decay):
        self.lr = lr
        self.weight_decay = weight_decay
    def compute_step(self, grads, params):
        step = list()
        # flatten all gradients
        flatten_grads = np.concatenate([np.ravel(v) for grad in grads for v in grad.values()])
        # compute step
        flatten_step = self._compute_step(flatten_grads)
        # reshape gradients
        p = 0
        for param in params:
            layer = dict()
            for k, v in param.items():
                block = np.prod(v.shape)
                _step = flatten_step[p:p+block].reshape(v.shape)
                _step -= self.weight_decay * v
                layer[k] = _step
                p += block
            step.append(layer)
        return step

    def _compute_step(self, grad):
        raise NotImplementedError

model 类实现了我们一开始设计的三个接口 forward、backward 和 apply_grad

class Model(object):
  def __init__(self, net, loss, optimizer):
      self.net = net
      self.loss = loss
      self.optimizer = optimizer

  def forward(self, inputs):
      return self.net.forward(inputs)

  def backward(self, preds, targets):
      loss = self.loss.loss(preds, targets)
      grad = self.loss.grad(preds, targets)
      grads = self.net.backward(grad)
      params = self.net.get_parameters()
      step = self.optimizer.compute_step(grads, params)
      return loss, step

  def apply_grad(self, grads):
    for grad, (param, _) in zip(grads, self.net.get_params_and_grads()):
      for k, v in param.items():
          param[k] += grad[k]

执行层

上面的抽象组件这么热闹,到真正的实现就又是另一幅天地了,可以好好品味 上层model 抽象与底层数据流图的gap,layer1 ==> layer2 ==> …layern 被展开成了 op,tenor 在layer 之间的流动 转换为了 dag op 间的流动。深度学习分布式训练的现状及未来AI 模型训练任务流程:初始化模型参数 -> 逐条读取训练样本 -> 前向、反向、参数更新 -> 读取下一条样本 -> 前向、反向、参数更新 -> … 循环,直至收敛。在执行层面的体现就是计算机按顺序运行一个个 OP。

几乎所有的 AI 框架都有 OP 的概念,简单来说就是一个函数,完成某个具体的功能,比如说加法、矩阵乘法、卷积等。为什么要多此一举引入这样一个概念呢?这其实是给每个具体计算功能抽象出一个统一接口,在静态图场景下能实现函数的编排(OP 的自由组合)。

极简demo

动手学深度学习框架(4)- 手把手教你写一个功能完整的简易 Demo

#include <iostream>
#include <iomanip>
#include <string>
#include <unordered_map>
#include <random>
#include <chrono>
#include <any>
//自定义 Tensor 类型,这里数据成员非常简单,就是个标量,重载了基本数学运算符
class MyTensor {
public:
    uint32_t data;
public:
    MyTensor(){};
    MyTensor(uint32_t x) : data(x) {}
    MyTensor operator*(const MyTensor& a) {
        this->data = this->data * a.data;
        return *this;
    }
    MyTensor operator+(const MyTensor& a) {
        this->data = this->data + a.data;
        return *this;
    }
    MyTensor operator-(const MyTensor& a) {
        this->data = this->data - a.data;
        return *this;
    }
    MyTensor operator*(const int& a) {
        this->data = this->data * a;
        return *this;
    }
};

// Op 基类
class OpBase {
public:
    std::unordered_map<std::string, MyTensor> inputs;
    std::unordered_map<std::string, MyTensor> outputs;
    std::unordered_map<std::string, MyTensor> labels;
public:
    virtual void Run() = 0;
};

// 乘法前向 Op
class MultipylyForward : public OpBase {
public:
    void Run() {
        MyTensor x = inputs["X"];
        MyTensor w = inputs["W"];
        MyTensor y1 = x * w;
        outputs["Y"] = y1;
    }
};

// 乘法反向 Op
class MultipylyBackward : public OpBase {
public:
    void Run() {
        MyTensor x = inputs["X"];
        outputs["Y"] = x;
    }
};

// 加法前向 Op
class AddForward : public OpBase {
public:
    void Run() {
        MyTensor x1 = inputs["X1"];
        MyTensor x2 = inputs["X2"];
        MyTensor y = x1 + x2;
        outputs["Y"] = y;
    }
};

// 加法反向 Op
class AddBackward : public OpBase {
public:
    void Run() {
        MyTensor x;
        x.data = 1;
        outputs["Y"] = x;
    }
};

// loss 前向 Op,这里选取 MSE 作为示例
class LossForward : public OpBase {
public:
    void Run() {
        MyTensor y = inputs["X"];
        MyTensor label = labels["Label"];
        MyTensor loss = (y - label) * y - label;
        outputs["Y"] = loss;
    }
};

// loss 反向 Op
class LossBackward : public OpBase {
public:
    void Run() {
        MyTensor y = inputs["X"];
        MyTensor label = labels["Label"];
        outputs["Y"] = (y - label) + (y - label);
    }
};

// 梯度更新 Op
class UpdateGrad : public OpBase {
public:
    double lr = 0.1;
    std::unordered_map<std::string, MyTensor> inputs;
    std::unordered_map<std::string, MyTensor> outputs;
public:
    void Run() {
        MyTensor w = inputs["W"];
        MyTensor grad = inputs["Grad1"] * inputs["Grad2"] * inputs["Grad3"];  // 链式求导
        MyTensor lr;
        lr.data = this->lr;
        outputs["Y"] = w - lr * grad;
    }
};

int main() {
    //1. 用户自定义前向组网
    std::vector<std::string> program{"Multiply", "Add", "Loss"};

    //2. 框架生成前向op + 自动补全反向OP + 插入梯度更新op
    std::vector<std::string> ops{"multiply_forward", "add_forward", "loss_forward",
        "loss_backward", "Add_forward", "multiply_backward", "update_grad"};

    //3. 实例化 c++ 端 op 对象
    std::vector<OpBase*> opClass {new MultipylyForward(), new AddForward(), new LossForward(),
        new LossBackward(), new AddBackward(), new MultipylyBackward(), new UpdateGrad()};

    //4. 框架根据用户组网,自动给每个op的输入赋值,这里仅以乘法前向op作个例子。一定要记住一点:框架中所有输入数据、
    //参数、模型中间输入、输出、以及每个参数的梯度都有一个 string 类型的名字,它的存在是为了给op输入赋值服务的
    opClass[0]->inputs["X"] = MyTensor(10);
    opClass[0]->inputs["W"] = MyTensor(20);
    for (auto op : opClass) {
        op->Run();
    }

    //5. 测试第1个op的输出
    std::cout << opClass[0]->outputs["Y"].data;  // 输出结果:200
}

分布式

深度学习分布式训练框架的运行机制

每个进程启动后,它需要感知自己全局的进程数( world_size)及自身的进程 ID(或者 rank_id),由于每个进程上运行的都是同一份训练脚本,所以得事先在每个进程所在的系统上设置不同的环境变量,进程运行起来之后,就可以获取环境变量,从而确定自己的角色(Worker、PServer、Coordinator 等)及rank_id、world_size 等信息。

在运行过程中,还有两个重要的环节是 Barrier 和 Communicate. Barrier 的目的是为了实现进程间同步,比较成熟的开源项目有 gloo、mpi 等。Communicate 操作就是实现通信,满足进程间数据交换需求。通信可以在同类型硬件之间发生,比如 CPU 到 CPU、GPU 到 GPU,也可以发生在不同硬件之间,比如 GPU 到 CPU,通信后端也有多种形式,比如 grpc、nccl、socket 等。

大模型时代

大模型时代,AI基础软件的关注点再次回到运行效率

  1. 以前AI生态锚点主要是在AI框架的API上,丰富易用的API是AI框架被开发者广泛接受的基础,基于这些API开发的海量模型是整个生态系统的基础。在大模型时代,由于大模型超强的泛化性和大量的智能涌现的特征,特别是类ChatGPT语言大模型的出现,基于基础预训练模型的微调成为主流的模型开发模式,AI的生态锚点会从框架的API转移到预训练模型上;而且,大模型本身就是生态入口。例如ChatGPT、GPT-4等大模型,它甚至可以进行任务的规划和分解,这种能力根本无法通过传统模型实现。这种强大的能力会催生更多的产品和应用原生地长在这些大模型上,进而成为了AI框架的生态锚点。比如,AutoGPT项目,它可以接受用户设定的任务,然后通过GPT-4的能力进行任务的分解,并协同其他工具完成用户设定的任务。AutoGPT三月份开源,目前Star数量已经超过了13万。
  2. 规模都超过千亿参数,甚至达到了万亿参数规模,训练这些大模型往往需要用到千张加速卡集群,从零开始训练的周期也是以月为单位计数。而从大模型的发展趋势来看,模型的参数可能还会持续增长,但是底层芯片的算力增长速度功能已经远落后于模型参数,导致模型规模和算力规模之间出现鸿沟,而这个鸿沟需要AI框架进行弥补,这很大程度上会使得AI框架关注的重点再次回到运行效率和可靠性上
    1. 静态图重新占有优势,在超大规模集群上,动态图并不能比静态图有更好的易用性,相反静态图有更好的性能和内存使用率,这个与小模型是不一样的。
    2. 大模型的分布式并行切分变得更加关键,从大模型训练的角度,如何提高上千张加速卡的运行效率?核心问题是通过编程将几千亿的模型参数合理的划分到这些加速卡上,并尽可能减少通信和内存搬移开销,这就需要框架支持灵活的并行切分模式;另外,如果这个过程全部靠工程师手动编程,则需要耗费大量的时间对系统性能进行调试调优,即使大模型发展这么久了,参与大模型工作的系统优化方面的工程师仍然至少占到1/3以上,这个问题的关键挑战是如何简化分布式编程的复杂度,使得工程师可以像单机编程一样,为分布式AI集群编程。为此,需要重点解决三个方面的问题:
      1. 支持足够多的并行策略,满足各种大模型高效训练的要求;
      2. 通过感知模型和硬件全局信息,做到全局配置优化;
      3. 在合理时间内能够自动搜索并生成能够最高的分布式并行策略,减少人工编程和调优的开销。
    3. 分布式并行训练长稳运行的必要性:在千卡集群中,硬件失效是常态,如何保证系统稳定可靠的运行,即便有硬件失效,也能快速恢复,就成为一个关键的能力。
  3. AI推理需要解决端侧部署大模型的挑战。由于大模型需要消耗大量的计算和内存资源,现有的大模型基本都是基于云环境提供服务,这会限制大模型的应用场景。比如,在端侧,很多涉及用户隐私的任务是无法发回到云环境执行的,这会导致大模型在ToC业务场景中受限。大模型在端侧的部署和应用主要有两种方式:
    1. 端侧部署的大模型独立完成任务,虽然有一些方法能让大模型在端侧跑起来,但是占用的内存和计算资源可能会影响其他应用,同时压缩后的模型的效果也没有经过全面的测试,能够支撑的使用场景应该是有限的。
    2. 通过端侧和云侧的协同将大模型的能力应用在端侧。在端侧对用户的请求做初步处理,将更加复杂的任务发挥到云侧大模型去执行,这种模式下,推理时延的优化尤为重要。

推荐文章

动手学深度学习框架(1) - AI 模型是如何训练出来的 写的很不错。

姜碧野:当我们谈论机器学习框架时,我们在谈论什么?

  1. BIDMach的整体设计分三层,下层关注于底层硬件的性能并统一封装为矩阵运算和Actor间的通信操作;中间层是各种机器学习算法的计算图封装,而最上层则是面向用户设计的交互式机器学习工具。
  2. 一般来说,类似tensorflow/pytorch这样的深度学习框架,实际上是提供了一个可微计算图引擎,他们可以非常方便地构建一个可微的函数(如下图所示),然后基于数据去最小化损失函数,这种抽象使得深度学习变得非常简单和可行易用。但是到了工业界(搜索/推荐/广告),实际的机器学习问题远不止这么简单,我们要重新思考一下框架的职责。框架是只需要专注于做损失函数优化就可以呢?还是会有很多其它事情也要去负责呢?
    1. 比如图中所示的x、y样本数据本身的生成就是一个很大的问题:比如样例生成或者特征萃取。而且在互联网应用中,我们往往需要从流式的数据流中实时地完成x、y的生成。
    2. 到了工业界中,训练和推理很可能是要被分开去做的。因为在推理的时候你只需要处理一个固定的计算图,这就存在很多优化的空间。

    所以实际情况是,如果把框架的概念从一个单点应用扩展到一个可用的工业界框架后,就会包含很多模块:样本的处理、特征的处理、离线训练和在线推理,各种数据接口,一致性保障、资源管理和整个实验平台等等一系列工具。这些东西可能从广义来讲都算是框架中的一部分。

  3. 框架与算法共同进化。工程框架的发展是跟整个算法的红利包括跟整个业务的发展都有关联的。算法侧提出了一个新的结构,框架就需要去做适配和推理优化。框架的革新又会导致算法工程师可以去尝试更复杂更加有意思且更有深度的想法。整个过程就是一个共同进化共同演化的过程。

深度解析开源推荐算法框架EasyRec的核心概念和优势针对推荐流程的各个阶段,业界已经有很多的模型,这些模型大部分也有开源的实现,但是这些实现通常散落在Github的各个角落,其数据处理和特征构造的方式各有差异。如果我们想要在一个新的场景里面应用这些模型,通常需要做比较多的改动:

  1. 输入的改造,开源的实现的输入格式和特征构造通常和线上不一致,适配一个算法通常需要1-2周左右的时间,还难免因为对代码的不熟悉引入bug,如果要尝试5个算法的话,就需要5倍的改造时间。如果算法资源有限,这时候是不是就要忍痛割爱,放弃一些可能有效果的尝试了?
  2. 开源的实现很多只是在公开数据集上取得了比较好的效果,在公开数据集上的最优参数也不一定适合实际的场景,因此参数调优也需要较大的工作量;如果没有系统化的调参方法,很多算法也就是简单试一下,没有deep explore,哪来对算法的深入理解呢? 为什么看似简单的改进,你没有能够发现呢?
  3. 开源的实现用的是TensorFlow 1.4,而线上用的TensorFlow 2.3,好多函数的参数都变掉了
  4. 费了九牛二虎之力把模型效果调好了,发现上线也会有很多问题,比如训练速度太慢、内存占用太大、推理qps跟不上、离线效果好在线效果跪等等。

遇到这么多问题,你还有精力去做你的下一个idea吗?你还能斗志昂扬,坚持不懈的去探索新方向吗?刘童璇:阿里巴巴稀疏模型训练引擎DeepRec 可以学习针对稀疏场景如何去优化tensorflow。