Skip to content

Ansor log file 格式

log 文件读写部分的代码

log file 的每一行是一个 json,其格式为:

{
    "i": input,
    "r":results,
    "v": log_version,
}

这些变量对应的类型为:

MeasureInput input;
MeasureResult results;
std::string log_version;

inputs: MeasureInput

MeasureInput 类型头文件 MeasureInputNode 类型头文件

读写 MeasureInputNode 的 log 时调用的函数

input 部分的格式为:

[
    task,
    state
]

taskstate 对应的类型分别为 SearchTaskState

task:SearchTask

SearchTask 类型头文件 SearchTaskNode 类型头文件

读写SearchTask的log时调用的函数

task 部分的格式为:

[
    workload_key,
    target,
    hardware_params,
    target_host,  // if define else ""
    layout_rewrite_option,
    task_input_names
]

涉及变量的类型为:

String workload_key;
Target target;
HardwareParams hardware_params;
Target target_host;
LayoutRewriteOption layout_rewrite_option;
Array<String> task_input_names;

workload_key:String

workload_key 的生成见 make_workload_key.

workload_key 字符串格式为:[func_name, [args...]]

make_workload_key(func, args)

workload_key 格式为

"[func_name, [tenser_shapes...]]"

如果这个子任务是用户自己注册的函数,那么 func_name 就是函数名。

若是 TVM 从完整模型中抽取出的子任务,那么 func_name 是用该子任务的 DAG 计算出的 hash 值,默认是 hashlib.md5(str_dag).hexdigest(),其中 str_dag 字符串是由 ComputeDAG::PrintDAG 产生,形如:

p0 = PLACEHOLDER
p1 = PLACEHOLDER
T_matmul_NT(i, j) += (p0[i, k]*p1[j, k])
p2 = PLACEHOLDER
T_add(ax0, ax1) = (T_matmul_NT(ax0, ax1) + p2[ax0, ax1])

target:Target

Target 类型头文件 TargetNode 类型头文件

TargetNode::str() 函数定义

target 部分的格式为(其中 attrs 中每个元素都会转换为 -{key}={value} 格式的字符串):

"{kind} -keys={keys} {attrs}"
TargetKind kind;
Array<String> keys;
Map<String, ObjectRef> attrs;

hardware_params:HardwareParams

HardwareParams 类型头文件 HardwareParamsNode 类型头文件

读写HardwareParamsNode的log时调用的函数

该部分格式为:

[
    num_cores, 
    vector_unit_bytes,
    cache_line_bytes,
    max_shared_memory_per_block, 
    max_local_memory_per_block,
    max_threads_per_block,
    max_vthread_extent,
    warp_size
]

对应变量的类型都是 int

layout_rewrite_option:LayoutRewriteOption

LayoutRewriteOption 类型头文件

layout_rewrite_option 类型 LayoutRewriteOption 是个 enum class

enum class LayoutRewriteOption : int {
  NoRewrite = 0,
  InsertTransformStage = 1,
  RewriteForPreTransformed = 2,
};

state:State

State 类型头文件 StateNode类型头文件

读写 SearchNode 的log时调用的函数

state 部分的格式为:

[
    stages,
    transform_steps
]

对应变量的类型如下:

Array<Stage> stages;
Array<Step> transform_steps;

stages: Array<Stage>

读写 Array<Stage> 的log时调用的函数

即 stages 部分就是个空数组。

transform_steps:Array<Step>

读写 Array<Step> 的log时调用的函数

Step 类型头文件 StepNode 类型头文件

该部分的 json 格式为:(每个 step_1 是一个 Step 对应的 json)

[
    step_1,
    step_2,
    ...,
    step_n
]

由于 StepNode::WriteToRecord 是一个虚函数,因此输出 json 时调用的函数 WriteToRecord 的定义要在 StepNode 的子类(如 AnnotationStepNodeFuseStepNodePragmaStepNode 等)中寻找。

include/tvm/auto_scheduler/transform_step.h 中搜索 public StepNode 能找到都有哪些子类,这些子类的 WriteToRecord 的实现在 src/auto_scheduler/transform_step.cc 中。

results:MeasureResult

MeasureResult 类型头文件 MeasureResultNode 类型头文件

读写 MeasureResultNode 的log时调用的函数

result 部分输出格式:

[
    [cost in costs],
    error_no,
    all_cost,
    timestamp
]

涉及变量的类型:

Array<PrimExpr> costs;
int error_no;
double all_cost;
double timestamp;

error_no 值的含义见 MeasureErrorNOcpp)或 MeasureErrorNopythonpython),其中 0 代表无错误,4 代表运行时错误,6 代表编译超时,7代表运行超时。

其中 costs 是指执行时间,而 all_cost 指整个 measure 的耗时。

在同一服务器上多个 GPU 上同时进行 TVM tuning 时会出现的问题

如果在同一台服务器上运行多个 TVM tuning 任务,measure 过程中很容易发生编译超时的错误,从而导致 tuning 过程中进行的有效 measure 非常少。

这是因为,一旦编译时间超过设定的 time_out,TVM 会自动 kill 掉编译进程,并将该实现标记为 error_no=6。通过查看 TVM 源码,发现 TVM 默认的编译并行度 n_parallel 是设置为 multiprocessing.cpu_count(),即系统所有的 CPU 逻辑核数。因此,当 TVM tuning 无法使用全部的 CPU 内核,或者服务器中包含可使用全部 CPU core 的多个 TVM tuning 时,算子的编译时间会变得非常长,以至于被 TVM 误判为编译超时。

在逻辑核数为 56 的服务器上,设置 docker container 的 cpuset_cpus,使得 TVM tuning 仅可使用 8 个核,结果导致 tuning 过程中最多有 3/4 的 measure 都是 error_no=6

解决办法(两者取其一):

  1. 修改 TVM 源码,将源码中所有 multiprocessing.cpu_count() 替换为 len(os.sched_getaffinity(0))
sed -i -e '1i\import os' -e 's/multiprocessing.cpu_count()/len(os.sched_getaffinity(0))/g' $(grep -rl multiprocessing.cpu_count "$TVM_PATH/python")
  1. 调低 TVM tuner 的并行度,或者调高超时阈值:
# 修改并行度
builder = auto_scheduler.LocalBuilder(n_parallel=len(os.sched_getaffinity(0))) 
# 修改超时阈值
# builder = auto_scheduler.LocalBuilder(timeout=60) # 默认是15

tuner = auto_scheduler.TaskScheduler(tasks, task_weights, ...)
tune_option = auto_scheduler.TuningOptions(
    builder=builder
 # ...=...
)
tuner.tune(tune_option)

如何从 log 中复用调度

目标:从 log 的中读取 input 部分的 transform_steps,生成对应的 cuda 代码,然后在新的目标硬件上进行测试生成对应 result。

[Measure] 过程调用的 C++ 函数:SilentMeasure(task, inputs, results),即 verbose >= 1 时,输出 ........******** 类似输出的时候,调用的函数。

该 C++ 函数的 build 部分(即编译部分)调用 python 函数 local_builder_build,而运行部分调用 python 函数 local_run (如果 使用的是 rpc runner,那么运行部分调用的是 rpc_runer_run

inputs = []
for log_string in logs_list:
    measure_input, old_result = tvm.auto_scheduler.measure_record.load_record_from_string(log_string)
    inputs.append(measure_input)

n_parallel = 8
build_results = tvm.auto_scheduler.measure.local_builder_build(inputs, timeout, n_parallel)

number=3
repeat=1
min_repeat_ms=100

results = tvm.auto_scheduler.measure.local_run(inputs, build_results, timeout, number, repeat, min_repeat_ms)

测试结果:第一个很慢,慢启动