(二)Taich Python部分生命周期之初始化

首先需要说明一下,此处虽说是讲Python部分的生命周期,但在初始化时taichi仍然调用了一部分pybind封装的c++代码,其中大部分都是用于初始化配置,我们也会顺带讲解一下,我仍认为这一部分属于Python部分的生命周期,等真正进入JIT编译时,才算是进入到C++部分的生命周期。

1 示例代码

import taichi as ti

ti.init(arch=ti.cpu)

x = ti.field(ti.i32, shape=(3, 4))
y = ti.ndarray(int, shape=16)
z = int(100)


@ti.kernel
def play(arg0: int, arg1: int):
    tmp = 0

    if arg1:
        tmp = tmp + 4

    for i in range(10):
        tmp += 1

    print(tmp)

    assert z == 100
    print(z)
    
    for i in x:
        x[i] = 0
    
    for i in y:
        ti.atomic_add(y[i], 5)


play(10, True)

下面我们以上面那段代码为例,taichi程序会分为两个部分,一部分是属于Python scope中的,而另一部分被装饰器所修饰的代码是属于taichi scope中的,可以看到taichi scope中的也会调用到Python全局中的field变量,这一部分我们暂时不考虑,taichi中的全局field变量会通过特殊处理链接到taichi程序中去,我们分开两部分来看,我们首先先看不含这些全局变量的taichi程序的生命周期是如何的,即我们这次调试的代码为下面的情况:

import taichi as ti

ti.init(arch=ti.cpu)

# x = ti.field(ti.i32, shape=(3, 4))
# y = ti.ndarray(int, shape=16)
# z = int(100)


@ti.kernel
def play(arg0: int, arg1: int):
    tmp = 0

    if arg1:
        tmp = tmp + 4

    for i in range(10):
        tmp += 1

    print(tmp)

    # assert z == 100
    # print(z)
    #
    # for i in x:
    #     x[i] = 0
    #
    # for i in y:
    #     ti.atomic_add(y[i], 5)


play(10, True)

2 Tachi Init

可以看到要想加载taichi程序,我们会首先需要初始化taichi包,我这里使用的cpu作为后端进行运行。

def init(arch=None,
         default_fp=None,
         default_ip=None,
         _test_mode=False,
         enable_fallback=True,
         require_version=None,
         **kwargs):
    # Check version for users every 7 days if not disabled by users.
    _version_check.start_version_check_thread()

在运行init函数之前,taichi包中有很多全局变量,下面一个是会伴随我们整个生命周期的一个Python全局变量PyTaichi,他在taichi.lang包下的impl模块中被定义

class PyTaichi:
    def __init__(self, kernels=None):
        self.materialized = False
        self.prog = None
        self.compiled_functions = {}
        self.src_info_stack = []
        self.inside_kernel = False
        self.current_kernel = None
        self.global_vars = []
        self.grad_vars = []
        self.dual_vars = []
        self.matrix_fields = []
        self.default_fp = f32
        self.default_ip = i32
        self.default_up = u32
        self.target_tape = None
        self.fwd_mode_manager = None
        self.grad_replaced = False
        self.kernels = kernels or []
        self._signal_handler_registry = None

PyTaichi的初始化只是单纯的对成员函数赋初值。

而我们之前调用的init函数是一大串初始化配置的代码,在开始时会进行版本的检查

def start_version_check_thread():
    skip = os.environ.get("TI_SKIP_VERSION_CHECK")
    if skip != 'ON':
        # We don't join this thread because we do not wish to block users.
        check_version_thread = threading.Thread(target=try_check_version,
                                                daemon=True)
        check_version_thread.start()

此处开启多线程进行版本检查,实际运行函数为try_check_version

def try_check_version():
    try:
        os.makedirs(_ti_core.get_repo_dir(), exist_ok=True)
        version_info_path = os.path.join(_ti_core.get_repo_dir(),
                                         'version_info')
        cur_date = datetime.date.today()
        if os.path.exists(version_info_path):
            with open(version_info_path, 'r') as f:
                version_info_file = f.readlines()
                last_time = version_info_file[0].rstrip()
                cur_uuid = version_info_file[2].rstrip()
            if cur_date.strftime('%Y-%m-%d') > last_time:
                response = check_version(cur_uuid)
                write_version_info(response, cur_uuid, version_info_path,
                                   cur_date)
        else:
            cur_uuid = str(uuid.uuid4())
            write_version_info({'status': 0}, cur_uuid, version_info_path,
                               cur_date)
            response = check_version(cur_uuid)
            write_version_info(response, cur_uuid, version_info_path, cur_date)
    # Wildcard exception to catch potential file writing errors.
    except:
        pass

在这个函数中,第一次运行taichi程序时会创建了一个taichi_cache文件夹用于放置临时缓存,windows默认在C盘根目录。同样在第一次运行时会在taichi_cache文件夹中创建一个version_info文件,这个文件内容是一传版本字符串,由时期+uuid组成,如果当前日期大于最后一次更新日期,需要对版本进行更新,日期判断只精确到天。

之后回到init函数中。

current_dir = os.getcwd()

if require_version is not None:
    check_require_version(require_version)

if "packed" in kwargs:
    if kwargs["packed"] is True:
        warnings.warn(
            "Currently packed=True is the default setting and the switch will be removed in v1.4.0.",
            DeprecationWarning)
    else:
        warnings.warn(
            "The automatic padding mode (packed=False) will no longer exist in v1.4.0. The switch will "
            "also be removed then. Make sure your code doesn't rely on it.",
            DeprecationWarning)

if "default_up" in kwargs:
    raise KeyError(
        "'default_up' is always the unsigned type of 'default_ip'. Please set 'default_ip' instead."
    )
default_fp = deepcopy(default_fp)
default_ip = deepcopy(default_ip)
kwargs = deepcopy(kwargs)

之后获取到了当前运行的py文件所在的文件夹,对参数中进行了检测,如果存在不合法会进行警告和报错,对合法参数进行深拷贝复制。

def reset():
    global pytaichi
    old_kernels = pytaichi.kernels
    pytaichi.clear()
    pytaichi = PyTaichi(old_kernels)
    for k in old_kernels:
        k.reset()

之后进行reset操作,reset的具体实现在Impl中,清空当前全局的pytaichi变量中的kernel集合,并对每一个kernel进行复位。

cfg = impl.default_cfg()
cfg.offline_cache = True  # Enable offline cache in frontend instead of C++ side

spec_cfg = _SpecialConfig()
env_comp = _EnvironmentConfigurator(kwargs, cfg)
env_spec = _EnvironmentConfigurator(kwargs, spec_cfg)

# configure default_fp/ip:
# TODO: move these stuff to _SpecialConfig too:
env_default_fp = os.environ.get("TI_DEFAULT_FP")
	......

if default_fp is not None:
    impl.get_runtime().set_default_fp(default_fp)
if default_ip is not None:
    impl.get_runtime().set_default_ip(default_ip)

接下来就是进行配置设置,上面代码全为配置设置,获取的Config类是由C++ Pybind绑定的,如下:

struct CompileConfig {
  Arch arch;
  bool debug;
  bool cfg_optimization;
  bool check_out_of_bound;
  bool validate_autodiff;
  int simd_width;
  int opt_level;
  int external_optimization_level;
  int max_vector_width;
  bool packed;
  bool print_preprocessed_ir;
	 ......

  CompileConfig();
};

默认的配置都是对这些成员变量进行初始化,接下来的是一些特殊环境配置,我初始化时并没有进行设置,这些部分都是默认值,大部分的控制流都不会进入,之后是加入日志等级和gdb等信息。

# compiler configurations (ti.cfg):
for key in dir(cfg):
    if key in ['arch', 'default_fp', 'default_ip']:
        continue
    _cast = type(getattr(cfg, key))
    if _cast is bool:
        _cast = None
    env_comp.add(key, _cast)

unexpected_keys = kwargs.keys()

逐级遍历Config中的属性,将属性名作为键值,类型作为value存到env.comp中。

unexpected_keys = kwargs.keys()

if len(unexpected_keys):
    raise KeyError(
        f'Unrecognized keyword argument(s) for ti.init: {", ".join(unexpected_keys)}'
    )

之后是查看是否有不需要的参数,有则报错

get_default_kernel_profiler().set_kernel_profiler_mode(cfg.kernel_profiler)

# create a new program:
impl.get_runtime().create_program()

_logging.trace('Materializing runtime...')
impl.get_runtime().prog.materialize_runtime()

impl._root_fb = _snode.FieldsBuilder()

if cfg.debug:
    impl.get_runtime()._register_signal_handlers()

os.chdir(current_dir)
return None

接下来就是初始化一些核心类用于创建和编译Kernel。这里首先会创建一个Program,这个Program可以视为一个整体的Taichi程序,会存放全部的filed等全局变量和所有的kernel。Program是一个c++类,通过Pybind绑定成Python对象,Program本身并不提供任何实现,具体实现有其子类提供,默认使用的是LLVM版本的Program,这段代码可以在taichi项目中的c++源码文件夹taichi下面的program文件夹中的program.cpp中找到:

Program::Program(Arch desired_arch) : snode_rw_accessors_bank_(this) {
  TI_TRACE("Program initializing...");
  .......
  main_thread_id_ = std::this_thread::get_id();
  ......

  profiler = make_profiler(config.arch, config.kernel_profiler);
  if (arch_uses_llvm(config.arch)) {
#ifdef TI_WITH_LLVM
    if (config.arch != Arch::dx12) {
      program_impl_ = std::make_unique<LlvmProgramImpl>(config, profiler.get());
    } else {
      // NOTE: use Dx12ProgramImpl to avoid using LlvmRuntimeExecutor for dx12.
#ifdef TI_WITH_DX12
      TI_ASSERT(directx12::is_dx12_api_available());
      program_impl_ = std::make_unique<Dx12ProgramImpl>(config);
#else
      TI_ERROR("This taichi is not compiled with DX12");
#endif
    }
#else
    TI_ERROR("This taichi is not compiled with LLVM");
#endif
  } else if (config.arch == Arch::metal) {
#ifdef TI_WITH_METAL
    TI_ASSERT(metal::is_metal_api_available());
    program_impl_ = std::make_unique<MetalProgramImpl>(config);
#else
    TI_ERROR("This taichi is not compiled with Metal")
#endif

我们下面的也都以默认的LLVM版本的Program实现进行讲解,后续有时间我会讲解其他backend实现。

让我们回到Python的init函数中去,在创建完Program后。调用了materialize_runtime方法来创建运行环境,我们具体来看一下LLVMProgramImpl的实现:

void materialize_runtime(MemoryPool *memory_pool,
                         KernelProfilerBase *profiler,
                         uint64 **result_buffer_ptr) override {
  runtime_exec_->materialize_runtime(memory_pool, profiler,
                                     result_buffer_ptr);
}

额,又包了一层,这里调用的是其执行器的初始化方法,继续往下走可以看到这里是对jit进行一些配置。到此为止,超长的Init函数就看完了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值