一个Job在OneFlow中的执行过程—中篇

上一篇文章《一个Job在OneFlow中的执行过程—上篇》,以bottom up的角度,简单讲解了一个Job(用户定义的训练/预测任务)在Oneflow中的调用入口、数据流转过程、从python端到c++端的代码执行流程,其中重点部分—编译期和运行时只做了简单介绍并未展开,本文将重点展开编译期过程,详细梳理编译期过程细节、代码执行流程。下一篇文章将详细梳理运行时过程!

需要特别说明的是,此系列文章仅在于描述流程、重点模块的代码,oneflow正处于快速版本更新和迭代过程,所以很多api在未来可能会有较大幅度的变动。目前在推进的工作有interface 1.0工作,包含了对齐pytorch api、multi client设计、构图编译期优化等,敬请期待!


概述

从整体上,Job编译分为两个部分:

  • Job逻辑图编译

  • Job物理图编译

其中,Job逻辑图的编译,主要基于由op节点(OpNode)构成的Job逻辑图(OpGragh),进行了一系列pass的系统优化过程,每个pass对逻辑图进行了一次图修改/重写(对逻辑图中的节点和连边进行了增删操作)。 Job的物理图编译,则是从Job逻辑图(OpGragh)编译为实际运行时的物理计算图(TaskGraph)以及生成最终运行时计划Plan的过程。

整个过程基于图(Graph)的抽象进行展开。

Graph抽象

图(Graph)作为一个基础抽象和数据结构,是所有深度学习框架里都存在的一个概念,因为Autograd往往都是建立在以图为基础的抽象之上(动态图或静态图)。同样,Graph[1]也是OneFlow中的一个重要基础抽象,OneFlow中各个重要的图相关的概念(OpGraph、LogicalGraph、TaskGraph、ChainGraph、ExecGraph...)都继承自Graph。Graph图的数据结构里保存着这个图中的所有的节点Node[2]和节点之间的连边Edge[3],基于Graph提供了一系列共用的遍历方法(普通遍历、拓扑遍历、BFS、DFS...),以及图改写(插入、删除 节点/边)图查询方法。Job的编译的过程也主要建立在OpGraph、TaskGraph等各种图相关的对象之上。

2.Job逻辑图编译

LazyJobBuildAndInferCtx::Complete()

在上一篇文章中,我们可以看到,通过在python代码中执行oneflow_api.CurJobBuildAndInferCtx_Complete()[4]即可调用c++中的LazyJobBuildAndInferCtx::Complete()[5]方法,此方法即为Job逻辑图编译的核心。

在该方法中,针对用户定义的user job,JobBuilder会基于Job的逻辑图OpGraph,对其进行一系列的pass优化,即代码中的DoPass。这些不同的pass优化有着不同的名称和作用,如用于自动混合精度的AutoMixedPrecision、用于并行场景下softmax交叉墒优化的SplitSparseSoftmaxCrossEntropyOpPass等等。

Maybe<void> LazyJobBuildAndInferCtx::Complete() {
  CHECK_NOTNULL(Global<JobDesc>::Get());
  Global<JobDesc>::Delete();
  if (job().job_conf().has_train_conf()) {
    CHECK_OR_RETURN(job().job_conf().train_conf().has_model_update_conf());
    CHECK_OR_RETURN(job().job_conf().train_conf().has_primary_lr());
  }
  auto scope = std::make_unique<GlobalJobDescScope>(mut_job()->job_conf(), job_id());
  JobPassCtx job_pass_ctx(GlobalJobDesc());
  auto DoPass = [&](const std::string& pass_name "&") -> Maybe<void> {
    return JobPass4Name(pass_name)(mut_job(), &job_pass_ctx);
  };
  if (GlobalJobDesc().Bool("__is_user_function__")) {
    JUST(DoPass("CompleteOfrecordDecoder"));
    JUST(DoPass("SetDefaultVariableConf"));
#ifdef WITH_CUDA
    JUST(DoPass("AutoMixedPrecision"));
#endif
    JUST(DoPass("OptimizerPlacementOptimizationPass"));
    JUST(DoPass("DynamicLossScaleSchedulePass"));
    JUST(DoPass("AutoTrainStep"));
    JUST(DoPass("AutoLearningRate"));
    JUST(DoPass("GenerateBackwardAndOptimizerOpConfs"));
    JUST(DoPass("AddSspVariableProxy"));
    JUST(DoPass("CheckpointingPass"));
    JUST(DoPass("CudnnFusedNormalizationAddReluPass"));
    JUST(DoPass("PruneCastToStaticShapeOpsPass"));
    JUST(DoPass("FuseAddToOutputPass"));
    JUST(DoPass("IndexedSlicesOptimizerRewritePass"));
    JUST(DoPass("SplitSparseSoftmaxCrossEntropyOpPass"));
    JUST(DoPass("DoParallelCastBeforeWideningTypeCast"));
    JUST(DoPass("AddLbiDiffWatcherOpConfs"));
    JUST(DoPass("FuseCastScalePass"));
    JUST(DoPass("PruneParallelCastOpsPass"));
    JUST(DoPass("FuseUpdateOpsPass"));
    JUST(DoPass("DumpVariableInfoPass"));
  }
  JUST(DoPass("DumpTimeShapeAndBlobParallelConfPass"));
  return Maybe<void>::Ok();
}

3.Job物理图编译

和逻辑图相对应,物理图则是包含了实际运行时需要的节点、设备、内存块等信息的,可实际执行的一种计算图。

在Job逻辑图编译完成后,会启动session然后执行Job物理图的编译和运行时计划Plan的生成过程。其中,具体地是通过python代码:oneflow_api.StartLazyGlobalSession()[6]调用了c++中的StartLazyGlobalSession()[7],从而启动session:

inline Maybe<void> StartLazyGlobalSession() {
  CHECK_NOTNULL_OR_RETURN(Global<SessionGlobalObjectsScope>::Get()) << "session not found";
  CHECK_OR_RETURN(Global<MachineCtx>::Get()->IsThisMachineMaster());
  const JobSet& job_set = Global<LazyJobBuildAndInferCtxMgr>::Get()->job_set();
  if (Global<ResourceDesc, ForSession>::Get()->enable_debug_mode()) {
    TeePersistentLogStream::Create("job_set.prototxt")->Write(job_set);
  }
  if (job_set.job().empty()) { return Error::JobSetEmptyError() << "no function defined"; }
  CHECK_ISNULL_OR_RETURN(Global<Oneflow>::Get());
  Global<CtrlClient>::Get()->PushKV("session_job_set", job_set);
  Global<const InterJobReuseMemStrategy>::New(job_set.inter_job_reuse_mem_strategy());
  Global<Oneflow>::New();
  JUST(Global<Oneflow>::Get()->Init(job_set));  # Oneflow::Init初始化
  return Maybe<void>::Ok();
}

session启动后,进行了Oneflow::Init的初始化,并在其中通过CompileAndMergePlanOnMaster()[8]方法完成了Job物理图编译、Plan生成等主要过程。CompileAndMergePlanOnMaster()方法主要做了以下工作:

  • 1.创建并添加系统级job

主要是添加一些模型io、数据输入/输出相关的系统级job至jobs vector,方便任务的开展。

  • 2.job物理图编译&plan生成

主要是遍历job vector中的所有job、对这些job(用户定义的user job作业函数、系统级model io job、pull job、push job等)进行图编译,生成task graph,并根据物理图生成该job的plan。

  • 3.子图plan间的融合

将所有job生成的独立的子图plan融合在一起,形成一个更大的merged plan。

  • 4.内存复用、临界区和可重入锁

内存复用主要是job之间的内存共享及复用;临界区和可重入锁则是通过添加另一种系统级的main job并在main job中加入CriticalSection临界区以及ReentrantLock可重入锁的设计,用于不同job间执行控制,目的在于增加job间执行的overlap,增加执行效率尽可能地流水并行起来。

  • 5.链接得到完整Plan

    链接main plan和merged plan得到最终完整的运行时计划Plan。

下面,我们将分别看一下每个过程的实现。

3.1 创建并添加系统级job

首先,在编译物理图之前,我们已经对用户定义的user job做了一系列pass优化、图重写的操作,user job处理完成后,并不能立即编译运行,还需要添加一些系统级别的job作为辅助,譬如:

  • model io相关的job用于模型存储

  • push job用于用户数据输入

  • pull job用于结果输出

添加系统级job相关的代码主要在CompileAndMergePlanOnMaster()[9]方法的908~950行。

Maybe<void> CompileAndMergePlanOnMaster(const PbRpf<Job>& conf_jobs, Plan* plan) {
  std::vector<std::shared_ptr<Job>> jobs(conf_jobs.size());
  FOR_RANGE(int, i, 0, jobs.size()) { jobs.at(i).reset(new Job(conf_jobs.Get(i))); }
  if (jobs.size() > 1) { CheckNonDistributeOptimizerAvailable(jobs); }
  if (Global<MachineCtx>::Get()->IsThisMachineMaster()) {
    //FilterOpName2ParallelBlobConf用于筛选出特定name的op,并返回ParallelBlobConf键值对
    HashMap<std::string, ParallelBlobConf> var_op_name2parallel_blob_conf;
    FilterOpName2ParallelBlobConf({OperatorConf::kVariableConf}, jobs,
                                  &var_op_name2parallel_blob_conf);
    auto AppendJob = [&](Job* job "&") {
      JobDesc job_desc(job->job_conf(), jobs.size());
      CHECK(!job_desc.Bool("__is_user_function__"));
      jobs.emplace_back(new Job(*job));
    };
    // 创建model io存储相关的job
    if (Global<const IOConf>::Get()->enable_legacy_model_io()) {
      if (Global<const IOConf>::Get()->enable_model_io_v2()) {
        MakeModelIoV2Jobs(jobs, var_op_name2parallel_blob_conf, AppendJob);
      } else {
        MakeModelIoJobs(jobs, var_op_name2parallel_blob_conf, AppendJob);
      }
    }
  }
  std::vector<std::shared_ptr<Job>> function_jobs;
  function_jobs.reserve(jobs.size());
  FOR_RANGE(int, i, 0, jobs.size()) {
    JobDesc job_desc(jobs.at(i)->job_conf(), i);
    if (job_desc.Bool("__is_user_function__")) { function_jobs.push_back(jobs.at(i)); }
  }
  if (Global<MachineCtx>::Get()->IsThisMachineMaster()) {
    HashMap<std::string, ParallelBlobConf> push_op_name2parallel_blob_conf;
    FilterOpName2ParallelBlobConf({OperatorConf::kInputConf}, function_jobs,
                                  &push_op_name2parallel_blob_conf);
    HashMap<std::string, ParallelBlobConf> pull_op_name2parallel_blob_conf;
    FilterOpName2ParallelBlobConf({OperatorConf::kReturnConf}, function_jobs,
                                  &pull_op_name2parallel_blob_conf);
    //创建push job并插入jobs
    for (const auto& pair : push_op_name2parallel_blob_conf) {
      auto push_job = std::make_shared<Job>();
      MakePushJob(std::string("System-Push-") + pair.first, pair.first, pair.second,
                  push_job.get());
      jobs.emplace_back(push_job);
    }
    //创建pull job并插入jobs
    for (const auto& pair : pull_op_name2parallel_blob_conf) {
      auto pull_job = std::make_shared<Job>();
      MakePullJob(std::string("System-Pull-") + pair.first, pair.first, pair.second,
                  pull_job.get());
      jobs.emplace_back(pull_job);
    }
  }
  ...
}
ParallelBlobConf

上述代码中,我们看见创建各种系统级Job时,依赖FilterOpName2ParallelBlobConf()方法和HashMap对象:

HashMap<std::string, ParallelBlobConf> xx_op_name2parallel_blob_conf;

其中通过FilterOpName2ParallelBlobConf()方法,我们可以根据job内每个op的类型名称获取该op对应的ParallelBlobConf对象。之后,根据xx_op_name2parallel_blob_conf的HashMap对象可以依次创建各种系统级Job(HashMap对象中的键为String类型的名称,表示job名称,值为protobuf对象—ParallelBlobConf)。此ParallelBlobConf对象是在job_conf.proto里定义的,带有sbp信息、设备信息的、用于描述分布式情况下张量blob信息的一种protobuf对象:

message ParallelBlobConf {
  required BlobDescProto logical_blob_desc_conf = 1;
  required ParallelConf parallel_conf = 2;
  required SbpParallel sbp_conf = 3;
  required OptInt64 batch_axis = 4;
}

protobuf是谷歌开源的用于结构化的消息(对象)传递的一种协议技术、可以做到平台无关、语言无关、多机器间对象的高效传递(序列化反序列化),可参考官方文档:https://developers.google.com/protocol-buffers/docs/cpptutorial[10]

在oneflow项目中使用了protobuf用于job、运行时计划plan等一系列对象间的多机对象传递,在项目代码中,可以看到有很多.proto结尾的文件,其中就定义了一系列用于传递protobuf消息的对象类型,类似的还有placement.proto里定义的用于描述张量存放的设备名称、类型的ParallelConf:

message ParallelConf {
  repeated string device_name = 1;
  required string device_tag = 2;
}

如blob_desc.proto里定义的用于描述张量信息的BlobDescProto:

message BlobDescProto {
  required StructPodProto header = 1;
  required TensorPodProto body = 2;
  required bool is_tensor_list = 3;
  required bool is_body_disabled = 4;
  required bool is_dynamic = 5;
  required bool header_is_opaque = 6;
}

3.2 job物理图编译&plan生成

上面添加系统级job相关的工作还主要是在job层面,接下来,将对这些jobs进行物理图的编译,每个job生成一个TaskGraph子图和运行时计划plan,并不断融合成一个更大到merged plan,这一部分的代码主要在CompileAndMergePlanOnMaster()的955行[11]之后的CompileCurJobOnMaster()方法中。

CompileCurJobOnMaster()

CompileCurJobOnMaster()是job编译的主要方法,其作用主要用于编译单个job并生成物理图,并根据物理图创建出该job的运行时计划plan。

Maybe<void> CompileCurJobOnMaster(Job* job, Plan* improved_plan, bool need_job_complete) {
  const JobDesc& job_desc = GlobalJobDesc();
  Plan naive_plan;
  Plan complete_plan;
  double start = GetCurTime();
  if (Global<MachineCtx>::Get()->IsThisMachineMaster()) {
    // 编译生成naive版的naive_plan
    Compiler().Compile(job, &naive_plan, need_job_complete);
    LOG(INFO) << "compile time: " << GetCurTime() - start;
    // 生成优化后的complete_plan
    complete_plan =
        *JUST(Improver().GenAndInferMemBlockIdOnly(*Global<AvailableMemDesc>::Get(), naive_plan));
    if (Global<ResourceDesc, ForSession>::Get()->enable_debug_mode()) {
      TeePersistentLogStream::Create("naive_plan")->Write(naive_plan);
      TeePersistentLogStream::Create("complete_plan")->Write(complete_plan);
    }
    LOG(INFO) << "push_pull_plan:" << GetCurTime() - start;
  }
  if (job_desc.enable_experiment_run()) {
    // 开启试跑模式
    if (Global<MachineCtx>::Get()->IsThisMachineMaster()) {
      PushPlan("complete_plan", complete_plan);
    } else {
      PullPlan("complete_plan", &complete_plan);
    }
    OF_SESSION_BARRIER();
    // Experiment Runtime
    { Runtime experiment_run(complete_plan, job_desc.piece_num_of_experiment_phase(), true); }
    // Improve
    if (Global<MachineCtx>::Get()->IsThisMachineMaster()) {
      TeePersistentLogStream::Create("available_mem_desc")->Write(*Global<AvailableMemDesc>::Get());
      CHECK_GT(Global<AvailableMemDesc>::Get()->machine_amd_size(), 0);
      *improved_plan = *JUST(Improver().Improve(
          *Global<AvailableMemDesc>::Get(), naive_plan,
          JoinPath(FLAGS_log_dir, ActEventLogger::experiment_act_event_bin_filename())));
      OF_SESSION_BARRIER();
      TeePersistentLogStream::Create("improved_plan")->Write(*improved_plan);
    }
  } else {
    // 普通模式,直接返回complete_plan
    *improved_plan = complete_plan;
  }
  // 生成集合通信boxing相关的plan
  GenCollectiveBoxingPlan(job, improved_plan);
  LOG(INFO) << "compile and improve time: " << GetCurTime() - start;
  return Maybe<void>::Ok();
}

其中,将job编译为物理图并生成plan的核心主要是其中的

Compiler().Compile(job, &naive_plan, need_job_complete);

Compiler::Compile()

Compiler().Compile()[12]方法为job编译生成物理图的核心,该方法主要将单个job生成物理图TaskGraph,然后遍历TaskGraph中的每个节点TaskNode,将实际负责任务执行的最小单位TaskNode插入到运行时计划—plan中,最终完成单个job物理图编译并生成plan的主要过程。

实际上,TaskGraph的生成过程是OneFlow编译期最重要也是最精华的一部分,Task是Actor的编译期抽象,Actor是Task的运行时抽象,所以TaskGraph就描绘了整个运行时物理计算图的全貌。以下内容只展示了物理图编译的大致流程,更详细的内容请参考源码和知乎文章:《仅此一文让您掌握OneFlow框架的系统设计(中篇)[13]

void Compiler::Compile(Job* job, Plan* plan, bool need_job_complete) const {
  // 编译job生成plan的主方法
  const JobDesc& job_desc = GlobalJobDesc();
  if (need_job_complete) { JobCompleter().Complete(job); }
  // 根据job创建OpGraph全局对象
  Global<OpGraph>::New(*job);
  if (Global<ResourceDesc, ForSession>::Get()->enable_debug_mode()) {
    TeePersistentLogStream::Create(StrCat("optimized_job", job_desc.job_id()))->Write(*job);
    Global<OpGraph>::Get()->ToDotWithFilePath("optimized_dlnet_" + std::to_string(job_desc.job_id())
                                              + "_op_graph.dot");
  }
  // 根据 job 生成逻辑图logical graph
  auto logical_gph = std::make_unique<LogicalGraph>(*job);
  // 根据 logical graph 生成task graph
  auto task_gph = std::make_unique<TaskGraph>(std::move(logical_gph));
  using std::placeholders::_1;
  task_gph->ForEachNode(std::bind(&TaskNode::ProduceAllRegstsAndBindEdges, _1));
  task_gph->ForEachNode(std::bind(&TaskNode::ConsumeAllRegsts, _1));
  task_gph->ForEachNode(std::bind(&TaskNode::PinConsumedRegst, _1));
  task_gph->TopoForEachNode(&TaskNode::Build);
  task_gph->RemoveEmptyRegsts();
  task_gph->MergeChainAndAddOrderingCtrlEdgeInSameChain();
  if (job_desc.enable_inplace()) {
    auto IsReachable = Global<OpGraph>::Get()->MakePredicatorIsOpNameDataOrCtrlReachable();
    task_gph->EnableInplaceMemSharing(IsReachable);
  }
  task_gph->TopoForEachNode(&TaskNode::InferTimeShapeIfMeaningful);
  // 遍历task graph中的每一个task node,将其添加至plan。task node为plan中实际任务执行的最小模块
  task_gph->ForEachNode([&](TaskNode* task_node "&") {
    if (task_node->IsMeaningLess()) { return; }
    task_node->ToProto(plan->mutable_task()->Add());
  });
  {
    auto* job_id2job_conf = plan->mutable_job_confs()->mutable_job_id2job_conf();
    (*job_id2job_conf)[GlobalJobDesc().job_id()] = GlobalJobDesc().job_conf();
  }
  // 销毁OpGraph全局对象
  Global<OpGraph>::Delete();
}

3.3 子图plan间的融合

经过上面job物理图编译和plan生成的过程后,所有的plan存放于vector中(sub_plans),这些plan最终需要合并成为一个整体plan,融合的过程主要在CompileAndMergePlanOnMaster()[14]方法第958行[15],通过:MergeSubPlanWithoutGenNetTopo(plan, sub_plans);完成子图plan(sub_plans)和主plan之间的融合。

3.4 内存复用、临界区和可重入锁

这一块的设计是oneflow中较为精华的所在,不仅涉及到了多个job间的内存共享和复用,还通过CriticalSection临界区以及ReentrantLock可重入锁的设计、使得多个job间的执行尽可能流水并行起来,具体的,是通过添加了一个系统级的main job完成了这些job的执行顺序控制,更详细的设计请参考同事的文章:《仅此一文让您掌握OneFlow框架的系统设计(上篇)》[16]

oneflow中,用户定义的训练、预测、验证等任务,我们称为user job;user job之外称为系统级job,典型的如:负责数据输入输出的push job、pull job;负责模型IO存储的model io job、内存复用job执行顺序控制的main job等

内存复用、临界区、main job相关的代码主要在CompileAndMergePlanOnMaster()[17]方法第957~971行[18]

if (Global<MachineCtx>::Get()->IsThisMachineMaster()) {
    // sub_plans子图融合融合至plan
    MergeSubPlanWithoutGenNetTopo(plan, sub_plans);
    // 设置内存复用相关
    InterJobMemSharingUtil::MergeMemReusedChunkBetweenUserJobs(function_jobs, plan);
    InterJobMemSharingUtil::MergeMemSharedInterfaceMemBlockBetweenJobs(jobs, plan);
    PlanUtil::SetForceInplaceMemBlock(plan);
    // 计算临界区
    FinishGlobalCriticalSectionDesc(*plan, jobs.size());
    Plan main_plan;
    std::vector<std::string> identity_tick_op_names;
    {
      Job main_job;
      LogicalBlobId critical_p_sink_lbi;
      // 创建main job
      MakeMainJob(&main_job, &identity_tick_op_names, &critical_p_sink_lbi);
      AddJobName2JobId(main_job.job_conf().job_name(), jobs.size());
      // 编译main job生成main plan
      JUST(CompileMainJob(&main_job, critical_p_sink_lbi, sub_plans.size(), &main_plan));
    }

3.5 链接得到完整Plan

将上面编译main job得到的main plan和主plan链接,得到完整的运行时计划—Plan链接的过程主要在CompileAndMergePlanOnMaster()[19]方法第972行:[20]

// 链接plan和main plan得到完整的运行时计划plan
LinkMainPlan(plan, main_plan, identity_tick_op_names);

4.运行时计划Plan的多机间同步

当主节点编译生成整个集群的运行时计划Plan后,会通过968行[21]PushPlan``(``"merged_plan"``, *plan)方法将Plan插入至protobuf对象,如果是分布式环境下,其余节点则会通过PullPlan``(``"``merged_plan``"``, plan)方法从protobuf环境中获取此Plan。只有在整个编译的过程中,是存在master的概念,在之后的实际执行中则是去中心化的,各节点根据自己的运行时计划Plan,执行各自的任务。

参考资料

[1]

Graph: https://link.zhihu.com/?target=https%3A//github.com/Oneflow-Inc/oneflow/blob/v0.2.0/oneflow/core/graph/graph.h

[2]

Node: https://link.zhihu.com/?target=https%3A//github.com/Oneflow-Inc/oneflow/blob/v0.2.0/oneflow/core/graph/node.h%23L74

[3]

Edge: https://link.zhihu.com/?target=https%3A//github.com/Oneflow-Inc/oneflow/blob/v0.2.0/oneflow/core/graph/node.h%23L46

[4]

oneflow_api.CurJobBuildAndInferCtx_Complete(): https://github.com/Oneflow-Inc/oneflow/blob/f733ab4cb43699cb4e3783032ac00b181166e8d9/oneflow/python/framework/compiler.py#L48

[5]

LazyJobBuildAndInferCtx::Complete(): https://github.com/Oneflow-Inc/oneflow/blob/f733ab4cb43699cb4e3783032ac00b181166e8d9/oneflow/core/job/job_build_and_infer_ctx.cpp#L939

[6]

oneflow_api.StartLazyGlobalSession(): https://github.com/Oneflow-Inc/oneflow/blob/f733ab4cb43699cb4e3783032ac00b181166e8d9/oneflow/python/framework/session_util.py#L221

[7]

StartLazyGlobalSession(): https://github.com/Oneflow-Inc/oneflow/blob/f733ab4cb43699cb4e3783032ac00b181166e8d9/oneflow/api/python/session/session.h#L67

[8]

CompileAndMergePlanOnMaster(): https://github.com/Oneflow-Inc/oneflow/blob/f733ab4cb43699cb4e3783032ac00b181166e8d9/oneflow/core/job/oneflow.cpp#L904

[9]

CompileAndMergePlanOnMaster(): https://github.com/Oneflow-Inc/oneflow/blob/f733ab4cb43699cb4e3783032ac00b181166e8d9/oneflow/core/job/oneflow.cpp#L908

[10]

https://developers.google.com/protocol-buffers/docs/cpptutorial: https://developers.google.com/protocol-buffers/docs/cpptutorial

[11]

CompileAndMergePlanOnMaster()的955行: https://github.com/Oneflow-Inc/oneflow/blob/f733ab4cb43699cb4e3783032ac00b181166e8d9/oneflow/core/job/oneflow.cpp#L955

[12]

Compiler().Compile(): https://github.com/Oneflow-Inc/oneflow/blob/f733ab4cb43699cb4e3783032ac00b181166e8d9/oneflow/core/job/compiler.cpp#L65

[13]

仅此一文让您掌握OneFlow框架的系统设计(中篇): https://zhuanlan.zhihu.com/p/338699487

[14]

CompileAndMergePlanOnMaster(): https://github.com/Oneflow-Inc/oneflow/blob/f733ab4cb43699cb4e3783032ac00b181166e8d9/oneflow/core/job/oneflow.cpp#L904

[15]

958行: https://github.com/Oneflow-Inc/oneflow/blob/f733ab4cb43699cb4e3783032ac00b181166e8d9/oneflow/core/job/oneflow.cpp#L958

[16]

《仅此一文让您掌握OneFlow框架的系统设计(上篇)》: https://zhuanlan.zhihu.com/p/337851255

[17]

CompileAndMergePlanOnMaster(): https://github.com/Oneflow-Inc/oneflow/blob/f733ab4cb43699cb4e3783032ac00b181166e8d9/oneflow/core/job/oneflow.cpp#L904

[18]

957~971行: https://github.com/Oneflow-Inc/oneflow/blob/f733ab4cb43699cb4e3783032ac00b181166e8d9/oneflow/core/job/oneflow.cpp#L957

[19]

CompileAndMergePlanOnMaster(): https://github.com/Oneflow-Inc/oneflow/blob/f733ab4cb43699cb4e3783032ac00b181166e8d9/oneflow/core/job/oneflow.cpp#L904

[20]

972行:: https://github.com/Oneflow-Inc/oneflow/blob/f733ab4cb43699cb4e3783032ac00b181166e8d9/oneflow/core/job/oneflow.cpp#L972

[21]

968行: https://github.com/Oneflow-Inc/oneflow/blob/v0.2.0/oneflow/core/job/oneflow.cpp#L968

网页端点击阅读原文,获得更好的阅读体验

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值