From 2800124d9d3d63908fb0e8d5f6c19ef9801290b3 Mon Sep 17 00:00:00 2001 From: sparanoid Date: Wed, 1 Nov 2023 09:53:54 -0700 Subject: [PATCH] feat: new version --- README.md | 10 + docusaurus.config.ts | 2 +- .../version-0.12.0/_static/css/tvm_theme.css | 43 + .../version-0.12.0/_static/img/README | 2 + .../_static/img/tvm-logo-small.png | Bin 0 -> 6683 bytes .../_static/img/tvm-logo-square.png | Bin 0 -> 3453 bytes .../version-0.12.0/arch/_category_.json | 3 + .../version-0.12.0/arch/arch/02-debugger.md | 148 ++ .../arch/arch/03-virtual_machine.md | 301 +++ ...04-introduction_to_module_serialization.md | 154 ++ .../arch/05-device_target_interactions.md | 78 + .../version-0.12.0/arch/arch/06-pass_infra.md | 493 +++++ .../version-0.12.0/arch/arch/07-inferbound.md | 679 ++++++ .../arch/arch/08-hybrid_script.md | 56 + .../arch/arch/09-relay_intro.md | 133 ++ .../arch/arch/10-relay_op_strategy.md | 173 ++ .../arch/arch/11-convert_layout.md | 249 +++ .../version-0.12.0/arch/arch/12-benchmark.md | 101 + .../version-0.12.0/arch/arch/13-tensorflow.md | 227 ++ .../version-0.12.0/arch/arch/14-security.md | 18 + .../arch/arch/15-microtvm_design.md | 233 ++ .../arch/arch/16-microtvm_project_api.md | 82 + .../arch/arch/17-model_library_format.md | 98 + .../arch/arch/runtimes/_category_.json | 3 + .../arch/arch/runtimes/index.md | 214 ++ .../arch/arch/runtimes/vulkan.md | 61 + versioned_docs/version-0.12.0/arch/index.md | 274 +++ versioned_docs/version-0.12.0/conf.py | 62 + .../version-0.12.0/contribute/_category_.json | 3 + .../version-0.12.0/contribute/ci.md | 103 + .../version-0.12.0/contribute/code_guide.md | 87 + .../version-0.12.0/contribute/code_review.md | 105 + .../contribute/committer_guide.md | 47 + .../version-0.12.0/contribute/community.md | 28 + .../version-0.12.0/contribute/document.md | 133 ++ .../contribute/error_handling.md | 84 + .../version-0.12.0/contribute/git_howto.md | 101 + .../version-0.12.0/contribute/pull_request.md | 134 ++ .../contribute/release_process.md | 164 ++ .../version-0.12.0/contribute_idx.md | 29 + .../version-0.12.0/dev/_category_.json | 4 + versioned_docs/version-0.12.0/dev/how_to.md | 11 + .../dev/how_to/01-debugging_tvm.md | 31 + .../dev/how_to/02-relay_add_op.md | 363 ++++ .../dev/how_to/03-relay_add_pass.md | 259 +++ .../how_to/04-relay_bring_your_own_codegen.md | 926 ++++++++ .../05-python_target_parametrization.md | 128 ++ versioned_docs/version-0.12.0/dev/index.md | 7 + .../dev/tutorial/codebase_walkthrough.md | 231 ++ .../getting_started/_category_.json | 4 + .../version-0.12.0/how_to/11-errors.md | 36 + .../version-0.12.0/how_to/12-FAQ.md | 26 + .../version-0.12.0/how_to/_category_.json | 3 + .../autoscheduler/01-autoschedule_gpu.md | 1214 +++++++++++ .../autoscheduler/02-autoschedule_x86.md | 552 +++++ .../autoscheduler/03-autoschedule_nvidia.md | 528 +++++ .../autoscheduler/04-autoschedule_arm.md | 547 +++++ .../autoscheduler/05-autoschedule_mali.md | 491 +++++ .../autoscheduler/06-autoschedule_custom.md | 611 ++++++ .../how_to/autoscheduler/_category_.json | 3 + .../how_to/autoscheduler/index.md | 12 + .../how_to/autotune/01-tuning_nvidia.md | 1884 +++++++++++++++++ .../how_to/autotune/02-autotuning_nvidia.md | 342 +++ .../how_to/autotune/03-autotuning_x86.md | 264 +++ .../how_to/autotune/04-autotuning_arm.md | 352 +++ .../how_to/autotune/05-autotuning_mobile.md | 348 +++ .../how_to/autotune/_category_.json | 3 + .../version-0.12.0/how_to/autotune/index.md | 11 + .../how_to/compile/01-compile_pytorch.md | 200 ++ .../how_to/compile/02-compile_tensorflow.md | 289 +++ .../how_to/compile/03-compile_mxnet.md | 165 ++ .../how_to/compile/04-compile_onnx.md | 139 ++ .../how_to/compile/05-compile_keras.md | 150 ++ .../how_to/compile/06-compile_tflite.md | 209 ++ .../how_to/compile/07-compile_coreml.md | 128 ++ .../how_to/compile/08-compile_darknet.md | 235 ++ .../how_to/compile/09-compile_paddlepaddle.md | 152 ++ .../how_to/compile/10-compile_oneflow.md | 240 +++ .../how_to/compile/_category_.json | 3 + .../version-0.12.0/how_to/compile/index.md | 17 + .../how_to/deploy/01-deploy_c++.md | 25 + .../how_to/deploy/02-deploy_android.md | 19 + .../how_to/deploy/03-integrate.md | 38 + .../version-0.12.0/how_to/deploy/04-hls.md | 157 ++ .../how_to/deploy/05-relay_arm.md | 158 ++ .../how_to/deploy/06-relay_tensorrt.md | 169 ++ .../how_to/deploy/07-vitis_ai.md | 356 ++++ .../how_to/deploy/08-relay_bnns.md | 123 ++ .../how_to/deploy/_category_.json | 3 + .../deploy/deploy_models/01-deploy_android.md | 346 +++ .../deploy/deploy_models/02-deploy_nano.md | 224 ++ .../deploy/deploy_models/03-deploy_pi.md | 212 ++ .../deploy/deploy_models/04-compile_od.md | 199 ++ .../deploy/deploy_models/05-deploy_prequan.md | 266 +++ .../deploy_models/06-deploy_prequan_3.md | 259 +++ .../deploy/deploy_models/07-deploy_quan.md | 149 ++ .../deploy/deploy_models/08-hugging_face.md | 299 +++ .../deploy/deploy_models/09-deploy_ssd.md | 162 ++ .../deploy/deploy_models/_category_.json | 3 + .../how_to/deploy/deploy_models/index.md | 17 + .../version-0.12.0/how_to/deploy/index.md | 127 ++ .../how_to/extend/01-writing_pass.md | 169 ++ .../how_to/extend/02-pass_infra.md | 446 ++++ .../how_to/extend/03-pass_instrument.md | 569 +++++ .../how_to/extend/04-datatypes.md | 468 ++++ .../how_to/extend/_category_.json | 3 + .../version-0.12.0/how_to/extend/index.md | 10 + .../version-0.12.0/how_to/microtvm/01-aot.md | 160 ++ .../how_to/microtvm/02-autotune_microtvm.md | 278 +++ .../how_to/microtvm/03-tvm_arm.md | 449 ++++ .../how_to/microtvm/04-microtvm_vm.md | 102 + .../how_to/microtvm/05-microtvm_tflite.md | 331 +++ .../how_to/microtvm/06-microtvm_arduino.md | 510 +++++ .../how_to/microtvm/07-tvmc_micro.md | 143 ++ .../how_to/microtvm/_category_.json | 3 + .../version-0.12.0/how_to/microtvm/index.md | 13 + .../version-0.12.0/how_to/models/01-PAPI.md | 83 + .../how_to/models/_category_.json | 3 + .../version-0.12.0/how_to/models/index.md | 6 + .../how_to/optimize/01-cpu_conv.md | 634 ++++++ .../how_to/optimize/02-gpu_conv.md | 198 ++ .../how_to/optimize/03-tensorcore_conv.md | 546 +++++ .../how_to/optimize/_category_.json | 3 + .../version-0.12.0/how_to/optimize/index.md | 9 + .../how_to/relay/01-build_network.md | 429 ++++ .../how_to/relay/02-extenal_lib.md | 557 +++++ .../how_to/relay/03-pipeline.md | 283 +++ .../how_to/relay/04-relay_visualizer.md | 200 ++ .../how_to/relay/_category_.json | 3 + .../version-0.12.0/how_to/relay/index.md | 8 + .../how_to/te_schedules/01-primitive.md | 427 ++++ .../how_to/te_schedules/02-reduction.md | 366 ++++ .../how_to/te_schedules/03-math.md | 275 +++ .../how_to/te_schedules/04-scan_recurrent.md | 220 ++ .../how_to/te_schedules/05-external_tensor.md | 125 ++ .../how_to/te_schedules/06-tensorize.md | 341 +++ .../how_to/te_schedules/07-compute_reduce.md | 169 ++ .../te_schedules/08-tedd_visualization.md | 113 + .../how_to/te_schedules/_category_.json | 3 + .../how_to/te_schedules/index.md | 12 + versioned_docs/version-0.12.0/how_to_idx.md | 20 + versioned_docs/version-0.12.0/index.md | 17 + versioned_docs/version-0.12.0/install-idx.md | 23 + .../version-0.12.0/install/_category_.json | 3 + .../version-0.12.0/install/docker.md | 68 + .../version-0.12.0/install/from_source.md | 344 +++ .../version-0.12.0/install/nnpack.md | 82 + .../version-0.12.0/reference/_category_.json | 4 + .../version-0.12.0/reference/api/links.md | 10 + .../reference/api/python/auto_scheduler.md | 7 + .../reference/api/python/autotvm.md | 103 + .../reference/api/python/contrib.md | 145 ++ .../reference/api/python/driver.md | 15 + .../reference/api/python/error.md | 7 + .../reference/api/python/graph_executor.md | 7 + .../reference/api/python/index.md | 10 + .../version-0.12.0/reference/api/python/ir.md | 17 + .../reference/api/python/micro.md | 7 + .../reference/api/python/ndarray.md | 19 + .../reference/api/python/relay/analysis.md | 7 + .../reference/api/python/relay/backend.md | 23 + .../api/python/relay/dataflow_pattern.md | 7 + .../reference/api/python/relay/frontend.md | 7 + .../reference/api/python/relay/image.md | 7 + .../reference/api/python/relay/index.md | 7 + .../reference/api/python/relay/nn.md | 7 + .../reference/api/python/relay/testing.md | 43 + .../reference/api/python/relay/transform.md | 7 + .../reference/api/python/relay/vision.md | 7 + .../reference/api/python/rpc.md | 7 + .../reference/api/python/runtime.md | 7 + .../reference/api/python/target.md | 7 + .../version-0.12.0/reference/api/python/te.md | 11 + .../reference/api/python/tir.md | 23 + .../reference/api/python/topi.md | 25 + .../reference/api/python/vta/index.md | 46 + .../reference/langref/hybrid_script.md | 225 ++ .../version-0.12.0/reference/langref/index.md | 37 + .../reference/langref/relay_adt.md | 543 +++++ .../reference/langref/relay_expr.md | 684 ++++++ .../reference/langref/relay_op.md | 131 ++ .../reference/langref/relay_pattern.md | 572 +++++ .../reference/langref/relay_type.md | 385 ++++ .../version-0.12.0/reference/publications.md | 52 + .../version-0.12.0/topic/_category_.json | 4 + .../version-0.12.0/topic/microtvm/index.md | 39 + .../topic/vta/dev/_category_.json | 3 + .../version-0.12.0/topic/vta/dev/config.md | 36 + .../version-0.12.0/topic/vta/dev/hardware.md | 190 ++ .../version-0.12.0/topic/vta/dev/index.md | 12 + .../version-0.12.0/topic/vta/install.md | 413 ++++ .../topic/vta/tutorials/01-mat_mul.md | 490 +++++ .../topic/vta/tutorials/02-start_vta.md | 392 ++++ .../topic/vta/tutorials/03-deploy_mxnet.md | 320 +++ .../topic/vta/tutorials/04-deploy_darknet.md | 326 +++ .../topic/vta/tutorials/05-conv_opt.md | 755 +++++++ .../vta/tutorials/06-mat_mul_blocking.md | 556 +++++ .../topic/vta/tutorials/07-autotuning_alu.md | 325 +++ .../topic/vta/tutorials/08-autotuning_conv.md | 479 +++++ .../topic/vta/tutorials/_category_.json | 3 + .../topic/vta/tutorials/index.md | 24 + .../version-0.12.0/topic/vta_idx.md | 29 + .../version-0.12.0/tutorial/01-intro.md | 61 + .../version-0.12.0/tutorial/02-install.md | 23 + .../version-0.12.0/tutorial/03-compile.md | 347 +++ .../version-0.12.0/tutorial/04-tvmc_python.md | 206 ++ .../tutorial/05-python_AutoTVM.md | 599 ++++++ .../version-0.12.0/tutorial/06-tensor_expr.md | 1064 ++++++++++ .../version-0.12.0/tutorial/07-ops_AutoTVM.md | 304 +++ .../tutorial/08-ops_AutoScheduling.md | 280 +++ .../version-0.12.0/tutorial/09-tensorIR.md | 287 +++ .../version-0.12.0/tutorial/10-rpc.md | 218 ++ .../version-0.12.0/tutorial/11-quick_start.md | 253 +++ .../version-0.12.0/tutorial/12-uma.md | 195 ++ .../version-0.12.0/tutorial/13-TOPI.md | 460 ++++ .../version-0.12.0/tutorial/_category_.json | 3 + versioned_docs/version-0.12.0/tutorial_idx.md | 23 + .../version-0.12.0/user_guide/_category_.json | 4 + .../version-0.12.0-sidebars.json | 156 ++ versions.json | 1 + 220 files changed, 40675 insertions(+), 1 deletion(-) create mode 100644 versioned_docs/version-0.12.0/_static/css/tvm_theme.css create mode 100644 versioned_docs/version-0.12.0/_static/img/README create mode 100644 versioned_docs/version-0.12.0/_static/img/tvm-logo-small.png create mode 100644 versioned_docs/version-0.12.0/_static/img/tvm-logo-square.png create mode 100644 versioned_docs/version-0.12.0/arch/_category_.json create mode 100644 versioned_docs/version-0.12.0/arch/arch/02-debugger.md create mode 100644 versioned_docs/version-0.12.0/arch/arch/03-virtual_machine.md create mode 100644 versioned_docs/version-0.12.0/arch/arch/04-introduction_to_module_serialization.md create mode 100644 versioned_docs/version-0.12.0/arch/arch/05-device_target_interactions.md create mode 100644 versioned_docs/version-0.12.0/arch/arch/06-pass_infra.md create mode 100644 versioned_docs/version-0.12.0/arch/arch/07-inferbound.md create mode 100644 versioned_docs/version-0.12.0/arch/arch/08-hybrid_script.md create mode 100644 versioned_docs/version-0.12.0/arch/arch/09-relay_intro.md create mode 100644 versioned_docs/version-0.12.0/arch/arch/10-relay_op_strategy.md create mode 100644 versioned_docs/version-0.12.0/arch/arch/11-convert_layout.md create mode 100644 versioned_docs/version-0.12.0/arch/arch/12-benchmark.md create mode 100644 versioned_docs/version-0.12.0/arch/arch/13-tensorflow.md create mode 100644 versioned_docs/version-0.12.0/arch/arch/14-security.md create mode 100644 versioned_docs/version-0.12.0/arch/arch/15-microtvm_design.md create mode 100644 versioned_docs/version-0.12.0/arch/arch/16-microtvm_project_api.md create mode 100644 versioned_docs/version-0.12.0/arch/arch/17-model_library_format.md create mode 100644 versioned_docs/version-0.12.0/arch/arch/runtimes/_category_.json create mode 100644 versioned_docs/version-0.12.0/arch/arch/runtimes/index.md create mode 100644 versioned_docs/version-0.12.0/arch/arch/runtimes/vulkan.md create mode 100644 versioned_docs/version-0.12.0/arch/index.md create mode 100644 versioned_docs/version-0.12.0/conf.py create mode 100644 versioned_docs/version-0.12.0/contribute/_category_.json create mode 100644 versioned_docs/version-0.12.0/contribute/ci.md create mode 100644 versioned_docs/version-0.12.0/contribute/code_guide.md create mode 100644 versioned_docs/version-0.12.0/contribute/code_review.md create mode 100644 versioned_docs/version-0.12.0/contribute/committer_guide.md create mode 100644 versioned_docs/version-0.12.0/contribute/community.md create mode 100644 versioned_docs/version-0.12.0/contribute/document.md create mode 100644 versioned_docs/version-0.12.0/contribute/error_handling.md create mode 100644 versioned_docs/version-0.12.0/contribute/git_howto.md create mode 100644 versioned_docs/version-0.12.0/contribute/pull_request.md create mode 100644 versioned_docs/version-0.12.0/contribute/release_process.md create mode 100644 versioned_docs/version-0.12.0/contribute_idx.md create mode 100644 versioned_docs/version-0.12.0/dev/_category_.json create mode 100644 versioned_docs/version-0.12.0/dev/how_to.md create mode 100644 versioned_docs/version-0.12.0/dev/how_to/01-debugging_tvm.md create mode 100644 versioned_docs/version-0.12.0/dev/how_to/02-relay_add_op.md create mode 100644 versioned_docs/version-0.12.0/dev/how_to/03-relay_add_pass.md create mode 100644 versioned_docs/version-0.12.0/dev/how_to/04-relay_bring_your_own_codegen.md create mode 100644 versioned_docs/version-0.12.0/dev/how_to/05-python_target_parametrization.md create mode 100644 versioned_docs/version-0.12.0/dev/index.md create mode 100644 versioned_docs/version-0.12.0/dev/tutorial/codebase_walkthrough.md create mode 100644 versioned_docs/version-0.12.0/getting_started/_category_.json create mode 100644 versioned_docs/version-0.12.0/how_to/11-errors.md create mode 100644 versioned_docs/version-0.12.0/how_to/12-FAQ.md create mode 100644 versioned_docs/version-0.12.0/how_to/_category_.json create mode 100644 versioned_docs/version-0.12.0/how_to/autoscheduler/01-autoschedule_gpu.md create mode 100644 versioned_docs/version-0.12.0/how_to/autoscheduler/02-autoschedule_x86.md create mode 100644 versioned_docs/version-0.12.0/how_to/autoscheduler/03-autoschedule_nvidia.md create mode 100644 versioned_docs/version-0.12.0/how_to/autoscheduler/04-autoschedule_arm.md create mode 100644 versioned_docs/version-0.12.0/how_to/autoscheduler/05-autoschedule_mali.md create mode 100644 versioned_docs/version-0.12.0/how_to/autoscheduler/06-autoschedule_custom.md create mode 100644 versioned_docs/version-0.12.0/how_to/autoscheduler/_category_.json create mode 100644 versioned_docs/version-0.12.0/how_to/autoscheduler/index.md create mode 100644 versioned_docs/version-0.12.0/how_to/autotune/01-tuning_nvidia.md create mode 100644 versioned_docs/version-0.12.0/how_to/autotune/02-autotuning_nvidia.md create mode 100644 versioned_docs/version-0.12.0/how_to/autotune/03-autotuning_x86.md create mode 100644 versioned_docs/version-0.12.0/how_to/autotune/04-autotuning_arm.md create mode 100644 versioned_docs/version-0.12.0/how_to/autotune/05-autotuning_mobile.md create mode 100644 versioned_docs/version-0.12.0/how_to/autotune/_category_.json create mode 100644 versioned_docs/version-0.12.0/how_to/autotune/index.md create mode 100644 versioned_docs/version-0.12.0/how_to/compile/01-compile_pytorch.md create mode 100644 versioned_docs/version-0.12.0/how_to/compile/02-compile_tensorflow.md create mode 100644 versioned_docs/version-0.12.0/how_to/compile/03-compile_mxnet.md create mode 100644 versioned_docs/version-0.12.0/how_to/compile/04-compile_onnx.md create mode 100644 versioned_docs/version-0.12.0/how_to/compile/05-compile_keras.md create mode 100644 versioned_docs/version-0.12.0/how_to/compile/06-compile_tflite.md create mode 100644 versioned_docs/version-0.12.0/how_to/compile/07-compile_coreml.md create mode 100644 versioned_docs/version-0.12.0/how_to/compile/08-compile_darknet.md create mode 100644 versioned_docs/version-0.12.0/how_to/compile/09-compile_paddlepaddle.md create mode 100644 versioned_docs/version-0.12.0/how_to/compile/10-compile_oneflow.md create mode 100644 versioned_docs/version-0.12.0/how_to/compile/_category_.json create mode 100644 versioned_docs/version-0.12.0/how_to/compile/index.md create mode 100644 versioned_docs/version-0.12.0/how_to/deploy/01-deploy_c++.md create mode 100644 versioned_docs/version-0.12.0/how_to/deploy/02-deploy_android.md create mode 100644 versioned_docs/version-0.12.0/how_to/deploy/03-integrate.md create mode 100644 versioned_docs/version-0.12.0/how_to/deploy/04-hls.md create mode 100644 versioned_docs/version-0.12.0/how_to/deploy/05-relay_arm.md create mode 100644 versioned_docs/version-0.12.0/how_to/deploy/06-relay_tensorrt.md create mode 100644 versioned_docs/version-0.12.0/how_to/deploy/07-vitis_ai.md create mode 100644 versioned_docs/version-0.12.0/how_to/deploy/08-relay_bnns.md create mode 100644 versioned_docs/version-0.12.0/how_to/deploy/_category_.json create mode 100644 versioned_docs/version-0.12.0/how_to/deploy/deploy_models/01-deploy_android.md create mode 100644 versioned_docs/version-0.12.0/how_to/deploy/deploy_models/02-deploy_nano.md create mode 100644 versioned_docs/version-0.12.0/how_to/deploy/deploy_models/03-deploy_pi.md create mode 100644 versioned_docs/version-0.12.0/how_to/deploy/deploy_models/04-compile_od.md create mode 100644 versioned_docs/version-0.12.0/how_to/deploy/deploy_models/05-deploy_prequan.md create mode 100644 versioned_docs/version-0.12.0/how_to/deploy/deploy_models/06-deploy_prequan_3.md create mode 100644 versioned_docs/version-0.12.0/how_to/deploy/deploy_models/07-deploy_quan.md create mode 100644 versioned_docs/version-0.12.0/how_to/deploy/deploy_models/08-hugging_face.md create mode 100644 versioned_docs/version-0.12.0/how_to/deploy/deploy_models/09-deploy_ssd.md create mode 100644 versioned_docs/version-0.12.0/how_to/deploy/deploy_models/_category_.json create mode 100644 versioned_docs/version-0.12.0/how_to/deploy/deploy_models/index.md create mode 100644 versioned_docs/version-0.12.0/how_to/deploy/index.md create mode 100644 versioned_docs/version-0.12.0/how_to/extend/01-writing_pass.md create mode 100644 versioned_docs/version-0.12.0/how_to/extend/02-pass_infra.md create mode 100644 versioned_docs/version-0.12.0/how_to/extend/03-pass_instrument.md create mode 100644 versioned_docs/version-0.12.0/how_to/extend/04-datatypes.md create mode 100644 versioned_docs/version-0.12.0/how_to/extend/_category_.json create mode 100644 versioned_docs/version-0.12.0/how_to/extend/index.md create mode 100644 versioned_docs/version-0.12.0/how_to/microtvm/01-aot.md create mode 100644 versioned_docs/version-0.12.0/how_to/microtvm/02-autotune_microtvm.md create mode 100644 versioned_docs/version-0.12.0/how_to/microtvm/03-tvm_arm.md create mode 100644 versioned_docs/version-0.12.0/how_to/microtvm/04-microtvm_vm.md create mode 100644 versioned_docs/version-0.12.0/how_to/microtvm/05-microtvm_tflite.md create mode 100644 versioned_docs/version-0.12.0/how_to/microtvm/06-microtvm_arduino.md create mode 100644 versioned_docs/version-0.12.0/how_to/microtvm/07-tvmc_micro.md create mode 100644 versioned_docs/version-0.12.0/how_to/microtvm/_category_.json create mode 100644 versioned_docs/version-0.12.0/how_to/microtvm/index.md create mode 100644 versioned_docs/version-0.12.0/how_to/models/01-PAPI.md create mode 100644 versioned_docs/version-0.12.0/how_to/models/_category_.json create mode 100644 versioned_docs/version-0.12.0/how_to/models/index.md create mode 100644 versioned_docs/version-0.12.0/how_to/optimize/01-cpu_conv.md create mode 100644 versioned_docs/version-0.12.0/how_to/optimize/02-gpu_conv.md create mode 100644 versioned_docs/version-0.12.0/how_to/optimize/03-tensorcore_conv.md create mode 100644 versioned_docs/version-0.12.0/how_to/optimize/_category_.json create mode 100644 versioned_docs/version-0.12.0/how_to/optimize/index.md create mode 100644 versioned_docs/version-0.12.0/how_to/relay/01-build_network.md create mode 100644 versioned_docs/version-0.12.0/how_to/relay/02-extenal_lib.md create mode 100644 versioned_docs/version-0.12.0/how_to/relay/03-pipeline.md create mode 100644 versioned_docs/version-0.12.0/how_to/relay/04-relay_visualizer.md create mode 100644 versioned_docs/version-0.12.0/how_to/relay/_category_.json create mode 100644 versioned_docs/version-0.12.0/how_to/relay/index.md create mode 100644 versioned_docs/version-0.12.0/how_to/te_schedules/01-primitive.md create mode 100644 versioned_docs/version-0.12.0/how_to/te_schedules/02-reduction.md create mode 100644 versioned_docs/version-0.12.0/how_to/te_schedules/03-math.md create mode 100644 versioned_docs/version-0.12.0/how_to/te_schedules/04-scan_recurrent.md create mode 100644 versioned_docs/version-0.12.0/how_to/te_schedules/05-external_tensor.md create mode 100644 versioned_docs/version-0.12.0/how_to/te_schedules/06-tensorize.md create mode 100644 versioned_docs/version-0.12.0/how_to/te_schedules/07-compute_reduce.md create mode 100644 versioned_docs/version-0.12.0/how_to/te_schedules/08-tedd_visualization.md create mode 100644 versioned_docs/version-0.12.0/how_to/te_schedules/_category_.json create mode 100644 versioned_docs/version-0.12.0/how_to/te_schedules/index.md create mode 100644 versioned_docs/version-0.12.0/how_to_idx.md create mode 100644 versioned_docs/version-0.12.0/index.md create mode 100644 versioned_docs/version-0.12.0/install-idx.md create mode 100644 versioned_docs/version-0.12.0/install/_category_.json create mode 100644 versioned_docs/version-0.12.0/install/docker.md create mode 100644 versioned_docs/version-0.12.0/install/from_source.md create mode 100644 versioned_docs/version-0.12.0/install/nnpack.md create mode 100644 versioned_docs/version-0.12.0/reference/_category_.json create mode 100644 versioned_docs/version-0.12.0/reference/api/links.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/auto_scheduler.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/autotvm.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/contrib.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/driver.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/error.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/graph_executor.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/index.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/ir.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/micro.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/ndarray.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/relay/analysis.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/relay/backend.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/relay/dataflow_pattern.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/relay/frontend.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/relay/image.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/relay/index.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/relay/nn.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/relay/testing.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/relay/transform.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/relay/vision.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/rpc.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/runtime.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/target.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/te.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/tir.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/topi.md create mode 100644 versioned_docs/version-0.12.0/reference/api/python/vta/index.md create mode 100644 versioned_docs/version-0.12.0/reference/langref/hybrid_script.md create mode 100644 versioned_docs/version-0.12.0/reference/langref/index.md create mode 100644 versioned_docs/version-0.12.0/reference/langref/relay_adt.md create mode 100644 versioned_docs/version-0.12.0/reference/langref/relay_expr.md create mode 100644 versioned_docs/version-0.12.0/reference/langref/relay_op.md create mode 100644 versioned_docs/version-0.12.0/reference/langref/relay_pattern.md create mode 100644 versioned_docs/version-0.12.0/reference/langref/relay_type.md create mode 100644 versioned_docs/version-0.12.0/reference/publications.md create mode 100644 versioned_docs/version-0.12.0/topic/_category_.json create mode 100644 versioned_docs/version-0.12.0/topic/microtvm/index.md create mode 100644 versioned_docs/version-0.12.0/topic/vta/dev/_category_.json create mode 100644 versioned_docs/version-0.12.0/topic/vta/dev/config.md create mode 100644 versioned_docs/version-0.12.0/topic/vta/dev/hardware.md create mode 100644 versioned_docs/version-0.12.0/topic/vta/dev/index.md create mode 100644 versioned_docs/version-0.12.0/topic/vta/install.md create mode 100644 versioned_docs/version-0.12.0/topic/vta/tutorials/01-mat_mul.md create mode 100644 versioned_docs/version-0.12.0/topic/vta/tutorials/02-start_vta.md create mode 100644 versioned_docs/version-0.12.0/topic/vta/tutorials/03-deploy_mxnet.md create mode 100644 versioned_docs/version-0.12.0/topic/vta/tutorials/04-deploy_darknet.md create mode 100644 versioned_docs/version-0.12.0/topic/vta/tutorials/05-conv_opt.md create mode 100644 versioned_docs/version-0.12.0/topic/vta/tutorials/06-mat_mul_blocking.md create mode 100644 versioned_docs/version-0.12.0/topic/vta/tutorials/07-autotuning_alu.md create mode 100644 versioned_docs/version-0.12.0/topic/vta/tutorials/08-autotuning_conv.md create mode 100644 versioned_docs/version-0.12.0/topic/vta/tutorials/_category_.json create mode 100644 versioned_docs/version-0.12.0/topic/vta/tutorials/index.md create mode 100644 versioned_docs/version-0.12.0/topic/vta_idx.md create mode 100644 versioned_docs/version-0.12.0/tutorial/01-intro.md create mode 100644 versioned_docs/version-0.12.0/tutorial/02-install.md create mode 100644 versioned_docs/version-0.12.0/tutorial/03-compile.md create mode 100644 versioned_docs/version-0.12.0/tutorial/04-tvmc_python.md create mode 100644 versioned_docs/version-0.12.0/tutorial/05-python_AutoTVM.md create mode 100644 versioned_docs/version-0.12.0/tutorial/06-tensor_expr.md create mode 100644 versioned_docs/version-0.12.0/tutorial/07-ops_AutoTVM.md create mode 100644 versioned_docs/version-0.12.0/tutorial/08-ops_AutoScheduling.md create mode 100644 versioned_docs/version-0.12.0/tutorial/09-tensorIR.md create mode 100644 versioned_docs/version-0.12.0/tutorial/10-rpc.md create mode 100644 versioned_docs/version-0.12.0/tutorial/11-quick_start.md create mode 100644 versioned_docs/version-0.12.0/tutorial/12-uma.md create mode 100644 versioned_docs/version-0.12.0/tutorial/13-TOPI.md create mode 100644 versioned_docs/version-0.12.0/tutorial/_category_.json create mode 100644 versioned_docs/version-0.12.0/tutorial_idx.md create mode 100644 versioned_docs/version-0.12.0/user_guide/_category_.json create mode 100644 versioned_sidebars/version-0.12.0-sidebars.json diff --git a/README.md b/README.md index 67d22a62..61472bc9 100644 --- a/README.md +++ b/README.md @@ -49,3 +49,13 @@ static/img/docs/tvmai/tvmai.github.io/main/images/relay/dataflow.png ```bash sphinx-build -b html docs build ``` + +## 创建新版本 + +如果当前版本为 `0.12.0`,想升到 `0.13.0`,那么你需要先保存当前版本 + +```bash +pnpm run docusaurus docs:version 0.12.0 +``` + +然后编辑 `docusaurus.config.ts` 中 `versions.current.label` 为最新版本 `0.13.0` diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 267743e1..c5d4ee7c 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -42,7 +42,7 @@ const config: Config = { lastVersion: 'current', versions: { current: { - label: '0.12.0', + label: '0.13.0', }, }, remarkPlugins: [remarkMath], diff --git a/versioned_docs/version-0.12.0/_static/css/tvm_theme.css b/versioned_docs/version-0.12.0/_static/css/tvm_theme.css new file mode 100644 index 00000000..93f4ea4d --- /dev/null +++ b/versioned_docs/version-0.12.0/_static/css/tvm_theme.css @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +.rst-content .hidden-section { + display: none; +} + +.rst-toc .hidden-section { + display: none; +} + +nav .hidden-section { + display: inherit; +} + +.wy-side-nav-search { + background-color: #fff; + color: #333; +} + +.version{ + color: #404040 !important; +} + +.wy-nav-content { + max-width: 950px !important; +} diff --git a/versioned_docs/version-0.12.0/_static/img/README b/versioned_docs/version-0.12.0/_static/img/README new file mode 100644 index 00000000..414328cc --- /dev/null +++ b/versioned_docs/version-0.12.0/_static/img/README @@ -0,0 +1,2 @@ +The logo file in this repo is an exception due to the need of sphinx. +By default we avoid to put large binary blobs into this repo. \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/_static/img/tvm-logo-small.png b/versioned_docs/version-0.12.0/_static/img/tvm-logo-small.png new file mode 100644 index 0000000000000000000000000000000000000000..c3519fece55b24bddca3dcf6d825fcfdef5576b1 GIT binary patch literal 6683 zcmZ{J1yodDzxE6r0z-p@LyFYU(lLOvbV+x^2t!GCm(&PKcS(sz4xJ(m0!m4TltaVC z_x*qGz29Byp0)Pbdq4a6J-^szopmBc3+)(37l0C4pH1#bfYo;(1+t~mf8iU0s8VeeZs z#UB=MVTy(x002Je-v$I^y{7^IfHMv{25i65N~g9Zf|~WH+MS-uZV~Ugoh8p$H(=+;PUWwg zkpE~un2P^JMP=PwVD2^^9uNAGyyE|2{(sni`}sFt-QB_FA=1C`{D=QPvH#82uy=#I zJygxz!Ai*$ZsY#o>>q2sf4u$QjDNJmA%7G9ABFgLmH&l4lvxr_9P*znlf*-t4}Q2p zC~KAEWORIh2S$Erx--7ZTb^e8Y|)wwA#5+KG-F{6$t~#G@4zj{FNsyBUX20ujU0O20XktO^YxhvZDHsnaWN=czo%!o z2_jz0@jFg{(%1-b=!TV$SC(AnoIjt;EE$KZ&!HhY(zHmm6yB|0{cI9c$?_rr$ED~j zPyT)~?{&bdSDkP*H8DH~m)N@gnT0Z<$;G`c+q}7CrlZD0`jp&$H7}_)jljlA)|8jT z=#vc!Xv_{?I}JyO5khMzM$h+JTH+548`?^24)}qV0h|<4-(S-LH#wry*a_~DE*1!? zpT}k&JFPAS=~O80-Gni+%epeTO)7q_LwhygzMS+@Bc~FpzVt;3ow)~LLVy>D+#;qD zfPmK_<-}eT5NKlGu|`P$OJ*j%0~5QgOFQgOGjQP<27;(7*kt^dRItTUy9UG6FEy}a z6CAXNt_?AFLeC+cuVg0AJBK8v?`mqeku-?4V`1VU(TFw}9%kN#ojP~o?z>8Mrfp;+Eb;_s-sS&2v8p=(2||zY7+KZQjXHZF$waH$>PDMCOv$G4ulct zv<~>&(mO?)E|7Y<1ez8Dgmm zVuTi^7v`zUo9LJ_UdX>MY;pW%Z%SO{Do`WK+Gf(~HFS7*I9V4JT<~xYhas;p*6Ah8$nvV)BQ? z2nSG}Hdf-8-mt~e@S-4p3@4(PT^kI>qGFRZ2wN?4Yg+XmT$%K(s}IVEl@tw^%@)+a ziQ{1aC~%My1PjAff-^+RS>MK6VCOxRa%K`@b1B9gc_Ayih|po@nA&;#?4?|`cgm%l z=*=*1#Mqb!;c6H_G=_YdEJ#JJ!a(n!_&w4d=Kn`=q)WgCNROVjGdK+u$EYHr-eo-1 z=B(AH`$%&qJT=bcf0eUZ+%WX(dT;aVh{7miYN7WGu2eWtC0}s9VaM$>k<`K zQ5Aa>&`hM&&+v%gk9XA#_fb-@Jk);RN^8WDSL8)A9|sAd%wxYO=TGp}c$?>*WvTz_ z(r>{WkGqHt$aSqhVt()Dx?*wnbNJ$*1tx=|LdaCo%oZ?d%6#*L%B;E7tQC9xCXs)e ze~n)--^@E_{JkW@x^@T8!NzE-pAjil+l@0aU9mnc!`Au%Q?x{>pu{w1asU$s$^EK0A zJs0A1^25>&@Uc0)c`8Fh1TQ3?WXcD3C`_P%jCp17&&GdqWTXcGWw)o(cfG!QaXzAv z!3ejY-n?v)gzrfEOB?9L8_Dkrw0+%IsCKkJ5Uwj}LhbrGK(CY((7&b-?f*p&dL#V-85)_vg%m>l2~i`dSY&|wZZ%5 zVsYO&=Zvht2#Z&*uewP1^RqOrwwHS?>pIb__KfF(KGhYkn-y=|7X{2#neq{+ z4f^|xLOGoegq<{<1`_gmbW1Ggi2MOh2Qd`gvv~J7g@-f6Uam$ZRMVZ$L7dE{@*O_T zQ`P9;L~127#j_LC)^H2-PNR2@V`@F(el)0IleAG1Q*6cjQu^6T1Tz;fR5f?ECE0Z5lko2szI?r4D;6e0bxtnaH*h|_$HjKt_S03yU zJMK)<2;(tKZ7Dm0O-F->T)~<)Cq6+6!>`51iHVh-J5u)jYaYP$i%@MOdMy&p9`1_R z9XfF5&disgqEu(zgmwhZ)HWVytxD)6~ zG)+_(+YZk7WoZD41(EEcr)r2DpRbnEa;ZD}F;^nHx7t3!*{{5!)%W!tM)I2$Mu9*c zQ=7KzpB!Z=|6s;UbA;P%F?KPx7_V=a< zzqfg`R?vw#A1L&J8~OM{HT6O}D0Bn|UmVR*OYD^jfb}nL|)0ssgQzf<%p+eJn}B^mZyYz-oz7klS^L6 zVB}d)jbV`bzoY!=wEsjz`w!Pab{_Ry9-D0zYnWE1P>u1Ww)B42JgfVJWvyA-HvIU9 zvLWT_F=f07!koGP4V0eZen6CyL8oN=5mvplQbbul@u6)Mvli1gG zzHZQjADKJq_x+o>VK#6x079Ye-;w;S^&R|UPF49AVMzOiS~VS{%bpvvM{Qa}gPqTt z5_yM~-CWIfr#+cucWS$E;=hjZbsU1^gVPmXGm*49*k}VTAho45|9{`R!Mx&Xl`h2sj zRAtYruTg4jYkGLxy(+vEwr6g{E6S5?(Dt2H@mj~3Zpo&trs~GrH>G}I^0>{9L9Zq+ z6&Md7E^bb{)YBP_Yj-vS&{nHLrXOM<>nxYzy^Q$Z^rD>tk|RgxK`9g@0(YIkiv>+a zG*_9qfqB?RSCeRqf#ZDIBekQWM*`9C9r>S3Z2hoJzzQPkv$Lgj21j>UrSh0r8 z{W8CK8MPqbf^xP}*kb5xudAcn7`2%?+i4OB5ViIZ-fZw5WNZ=P$+t3uPpLVruMa#) zyG9+LhYYD8p8O$0>tY~kzVNSiY5j3gT4F<&eRO4GpVceUR|?YXz*si=Vj{Q5K*8pI zM=?M0Eg0C6tQTEE8F#<;_!D)^sXVup1ZZaxo@z(G?X7O)aqHid+aB01cuYs$tc8|T zUGPHc*Ld^my9&dG9JhM?CBVRrBb3T#`pGv*R4!qyIVKL z!Fa}N$x!VS&@QKugxy%Ki_sE7a6!8kV@?{DAbvwJepOMG-b{HU1O+f7WEr#rl*+b0 zpzA5dlHP;>cwz_2-g~EmJm~vlV^bv-e@qzW@8zux&^MS^BQ^IESb553#?=LMFtAUD zgi)3Alz$`(A~Ire?H4Ez{a?)2Vgx3RR3WaehNnBTMq9rl$n%)^$MGUG$w%T%LRoHg zvhI;du;(9ZDeqGdDf9k>YFWRufEL&7@IJaKY;In^vG?7{P0^ZpFP~tq8i?49?Il!K zI`ixYjtC_6eTj)1(lqC4>j1ib_g*+9`Wc?wD)E~YS%5ZmMUh`2QdS3IGa5HGuw=HJ zQswYsywk}oDXE466V?li5N=iPS}U% zBV#*&qtWqm2j-JC$#G{9>1^?6*_IUw97(J_f9z2w5aT`Cu7+#=DBA1F4XWLBd*?cI z;(ksgbkarkLQII%z&V8H)J$)PCNC)YJJrJ1cd?S1X!}fR$G{E^Lm|>Pf53(O7LjYS*RSpcW73xr=uIKscFea3y$&d zi{kw$TWjlwJ6iFPIHyt1{8A|5={q+nQ7>HwVN!JvVBuX)AGb1gckE}nLE4p*;--;~ zd+I4~byoD0!$6(rC!=VxwonfBvYIa-+0`Dc?e&Rc4)Y7@ZA!clS!i(ZBRAz&sq8VT zQ%+#TnptVZlb^XOS z?@EL;h_3|9-rt>>jjO^N5?-h@uyjY0A}ac7ZpX3EVx)txgN;PQ z_;a`mQT#o2Fn>mEZ5nNIM{5#{U8K5-Ln*wNO!@rwGw-`|2IP8OfXxj9p~V#$Zoy+| z9Mf*GQ{o~KforKqmS9{ibDl%2?dqed94n$ssi&f z7%(p<+(_XZrq3?z>+i|+BULil=9)=ua?T?Qc50mRP6?nBax2S(C)(9>{7l0U(@Jo{}(kp=;MRjLqTpr|wUaDHIYk)S(+Wky?=dMz|>cK~q{I<~#uAg}|E z%_E@PJX>gxB|FgeTsbL2uH5IXj-zp*Gt}??x^V)Sf^o!%VJ_;bn{(mIE_4PU{C{Hl>83g8fXMb}jwnB2qQxa2COmhvBBAP`#Wo2)L>3Wjz{EfnMU^B)ewcG7gSez4`8Usm? za=kkSD5l_@4kaE|s_|D(wu?f8f*s+2u9c;p-4+&tki68%7Y^7#QZTu&2y8h6m9_(( zTQTYtP5RHQ+;?_MS6w1|m=BLa(0hQ96dwHtDf$JTgr%$aHXFdD)g2d}^va~{9m8=L zsfwasMvryi*(iND(tgEU5EcMu;uoI=}Dgw0l$Y@&HZ5c zp>X%A=xDH2$4TUhB~M(3oXmop6VO9=gsvlNs>igui%<58)KCjr24r?0ueWw9e?#B* z#5lkIN9ZE2csE4LH9W{2eKud%@`MoUbFI9Kz45GRrSH!q{e?Kwr-`>d>5R}y9-VpH zm5WcLUnlvl�<59rn*Z!a?KV1NWH%x5g(ha~Hr9*CqXmc)-TX?`1S5-|I#>FHz2UTjSP<0k9z-aT{%9xjA&SE(q? z_CW2hPNd#54>}4DWJr1LHWbwsu)kTC>KB->$hSOX!;eMvD_G|BQdK)9{cd{m#RVBF zie2CmWVz(KM1_>Qd0%SOX&6w6(i?08?wi*>9=1Zmv@PO0+A(?eqi^V!uEhBu#S6e{PS96|UhV3=5c!MjagkcR{}8Bd+> z$`X6Yc9X{9+Y%G|W)nln8w;A644UTJFHaES-D7#F)RPsAoO;2C=nv1hdcw4K^!Wm5 zl`Pq)ri94akoOZ(@tDYYFlAB*nUas&+fr}#l$9(|v%^EZpcPPt{u>OJIEBwqVj$dp z=Z!zx$YdC^4*8WfMhxcRQ1nd?=)FAjW*gnVu*x|Y`@Jr-qS~}R`h2s43%(_4(AYTu zu-h&f>YAw%1~a~!e|FT(3E9+>X5eJimfd6oRY>d$U+$5VlW`}!V%)Q9>fE!=`ABGC z&Na#&T5{1!4tjMP0OihBWg(GY!fL9yi;zMg9a&Su3qy4iRa`W+blbPaU$G@*s8TEa zw8g_jcB5wFF@h!1$~W+FpxHfh;E*fbrO$x`*{5a8#d`{V(&i*^N$|t~ zP{jc3igm_0>oKZhU0mxGp~04BqkXnz)D3&mgFEPNJe0G)9f9WRarX^tvEMMN0C)41 zpHIQ_$^{=YzB`0pveQ z0s=~trig&j5d}j>N&xBY#Tnnsd++<+{l0V0IqR&w*53QvKR4dm(&z~2I0yiMBPPZO zTgGh27zj>w#v4AZpTd|}eRVB#0iY~}d(VT7v4om(*ZyPlUZv6XB=>0jGb`+z%BS=Fah^6MFD`R-_PC&?_^=FiA1B6J-pDK-pT|N zhQVf75;Pf0lsDc3LO=xu;WP=_Qhzcu8S5V~R0{GZ1s|X-J|1S7@=gg^yg06nknT7ut~b#KWPm+8ZXWw zq`-8KcC(CGSxxBiHGxtB#nku>H>U$Uf$&&Lk1c@=0)yCeK@>wBmj!~`8f|x}bG>V+ zYvJ8Xt&1%dzP{7vx+b({vhJSUtkqK!v4{&VtReNNe_rdHSySj$Tfc8CY{UZT6Z#Ss z@jgm~E0x3&RMH%Lm*1rty;Ym6laWELY9y3MQ4UzU#)N3EH!1S=SFaz-Jgw%nHZtPk z9~5M{amiIp?p<1IvDZEM+ZCqyo+ZV_!X_|2QZ!j&xw(aKf-_<^)D_BRlp9bE4b(m}ByGA72(VH9*ePWG@qfkVs4X3Dm^&c*Fj&0g;{hy8Ki?s=A zxb)hA^Nq_sl8%wXATXKPTx~fyC6n-?;M)UpF@@$}nd&2%PXr7u%*0HO55RalE^au# zD_(fH*?i^ThTT(Zc@1#%>un2>`gJn@oCDc2_vZBIhrLr%T?w&%x865~MJTnjt?Pw* zOLuT~29T(!zV4XVg z60@)Sy*b_yEG$1Rqeix6(r9}ZUF~%~fRY}d+R@o5HdhzpvvB77EmpQ`1z&YZRveye zZN6r$O5zi4)P7uv+)SmOF2~w1fSO=;4^}Ul*6WeY)^KlR!;&lMUv8YF-_cEHw>4-M zd^2TVerfjazCUKG1?r0wCw%le`Jmc5k+nwc8u>(O#=l0yP zu{tT{w$tb@C-Xf@ek=(g={}iTol$n!d-ZEok^#{Yxer^)&s8I>!$i-la1wbw$vfS& z^OJ$4Lev5MrkmeII}is!G){Fuy2+GoL{i*Jv8P+`r;rBX!?;`H>FF{^b8S77F9txG z*9|c)-_(sbK-NSnb@T8#+DcgC%2H;`3s5yRd}c1*un%%*kS}a#*n^Z0owR#9aY4d1 zm0BE-VJwK48w@+~hwcU+5fZN5m1!bV*P};RuTG9gc`I%8x=F!8>wTv<`DRV5Fsqgy zHdWAEGU@BM&38dwCpWiEDR{xO<$dc#^hr{b`FTWbbEnvOo5~s+xSzwb_I5d0Rr1H- zx4NNrc6OQf+-f(AbNQraa(}BF26&XH_-v0HyW8gWHgu~oWEKv*o0N;bV5yQ1B+$1Tlx61; zEU+Te!XEjGc&Z^CbysadfaCf0Bg^~0ARG0YqU_MobGFI$59uTdIa|XCCh$d6IjdWX zqN`9EdFk+-hE6nQ;BM|>{-g@$1edWLKOa-G$Ww|>YKf3ePbs3QuVCM^!Jj>3#a~|% zG@5toeAPZ0K2?j(mw8}l$|dCc{X%vOUkpWH4f_k~<+onbSCd`gCFMZOfqzk)=jYMA z7=DhUxGXBu2dmMHef&2km+U8#o6kDaOUpO4NzrhT`AwMS^Rkkyy1GR+|~!<9*4HZ1gIl= zU#PZJjMW7%RRZ{3MSQq*+~=_kT^o%+7X1-f3dEBm8QB!@mT%*<;^5{U zkM=PR7Sv0q?(tZl^yCDeFA)a5pasoqg@P72fVtBP6$Dol#y#H;t;ZQvq?U1#TZ zJ*;T?f~1n>bvOX8L~-yF&G@FrbwsmX)1K}=JX>z@k*n?P!5-5gn0H2zH&B0j0X-+< z(E*MQOXJA`HvzzD);zm0QPVLuGKt(bR+p-#{#rV}O$nQi!yPt565 z1j&zQg@&I>oZ^vqD?9x$JLT&lkU{O>*?4T0*NEKqF4<>`+S>7`e@cOSe+pw}ReV)L zjFmj*>dZdOAJ@zpFEyV&zl5-5g7$q zgjb0o9@2)+bD~={G&enXh(u$jd$QaK$|)J8C=bI#vFG%*ab9xZe9o+zy89RU{iaB5 zjkuH{Dh;no)PW`4JRMTYkVK+$A$n+J#avnILTlEpY0c$39j6}I`{QmU^6uV# z=PeO>I^!i#@!QqR5RWX-28_S>iDf9Y6Mnt)a`f z%ax(k!@_N!s_K@j@+Y8^YSQ~r{(W+^GQAPf2G%@Q!Ti_qgvCKyUa6>+;%}DuN#Cay zUe=#es#77kCKk*xu{=dRgH4Xkao5_1JVLM+M8LleG`|7Db#`&v zGww&WA7SJJe@n~YBCYfsA6_ac6}a29n)IA}*xur=2+I>MF~8#asn^Er^{BZ!oIHmp zxyBZDul_UFj8%Z*j`wq?d~Kzc1~j6uazsEB`_Q(xxA%BeN-h0b@?BLr)@Vv>VVeC& z8}+mSv5ThC86x=m^#hjSn2+)t(|lL7Fi2aW4AFYJ(PeGh6zd#`mIwOFEvEq0->nn- z?~CW#tPO0AuC-Uw2R)L2vmV`a$L(**qb9UD-kE^yme6DnN z(AyP!wJOPa(OgnkJ%9h@(Y<_fv+}yh&LOj{#AuJ>{pP$MKRG4_mWX0K_iO(G1A{>< literal 0 HcmV?d00001 diff --git a/versioned_docs/version-0.12.0/arch/_category_.json b/versioned_docs/version-0.12.0/arch/_category_.json new file mode 100644 index 00000000..facaa86a --- /dev/null +++ b/versioned_docs/version-0.12.0/arch/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 500 +} diff --git a/versioned_docs/version-0.12.0/arch/arch/02-debugger.md b/versioned_docs/version-0.12.0/arch/arch/02-debugger.md new file mode 100644 index 00000000..512b6e5e --- /dev/null +++ b/versioned_docs/version-0.12.0/arch/arch/02-debugger.md @@ -0,0 +1,148 @@ +--- +title: 调试器 +sidebar_position: 120 +--- + +# 调试器 + +TVM 调试器是一个用于调试 TVM 计算图执行过程的接口。它有助于访问 TVM runtime 的计算图结构和张量值。 + +## 调试交换格式 + +### 1. 计算图 + +由 Relay 以 JSON 序列化格式构建的优化图按原样转储。包含有关计算图的全部信息。UX 可以直接使用此图,也可以将此图转换为 UX 可以理解的格式。 + +Graph JSON 格式解释如下 + +1. `nodes` 节点是占位符,或是 json 中的计算节点。节点存储为列表。一个节点包含以下信息: + * `op` - 操作类型,`null` 表示它是一个占位符/变量/输入节点,`tvm_op` 表示该节点可以执行 + * `name` - 节点名称 + * `inputs` - 算子的输入位置,Inputs 是一个(nodeid、index、version)元组列表。(可选) + * `attrs` - 包含以下信息的节点属性 + * `flatten_data` - 执行前是否需要对数据进行展开 + * `func_name` - 融合函数名,对应 Relay 编译过程生成的 lib 的符号。 + * `num_inputs` - 节点的输入数量 + * `num_outputs` - 节点产生的输出数量 +2. `arg_nodes`:arg_nodes 是节点的索引列表,它是计算图的占位符/变量/输入或常量/参数。 +3. `heads`:heads 是计算图输出的条目列表。 +4. `node_row_ptr`:node_row_ptr 存储前向传播路径的历史记录,因此可以在推理任务中跳过构建整个计算图。 +5. `attrs`:attrs 包含版本号或类似的有用信息。 + * `storage_id` - 存储布局中每个节点的内存插槽 id。 + * `dtype` - 每个节点的数据类型(枚举值)。 + * `dltype` - 按顺序排列的每个节点的数据类型。 + * `shape` - 每个节点的 shape 为 k 阶。 + * `device_index` - 计算图中每个条目的设备分配。 + +转储图示例: + +``` json +{ + "nodes": [ # 节点列表 + { + "op": "null", # operation type = null,这是一个占位符/变量/输入或常量/参数节点 + "name": "x", # 参数节点的名称 + "inputs": [] # 此节点的输入,这里为 none,因为这是一个参数节点 + }, + { + "op": "tvm_op", # operation type = tvm_op,这个节点可以执行 + "name": "relu0", # 节点名称 + "attrs": { # 节点的属性 + "flatten_data": "0", # 此数据是否需要展开 + "func_name": "fuse_l2_normalize_relu", # 融合函数名,对应编译过程生成的 lib 的符号 + "num_inputs": "1", # 此节点的输入数量 + "num_outputs": "1" # 此节点产生的输出数量 + }, + "inputs": [[0, 0, 0]] # 此操作的输入位置 + } + ], + "arg_nodes": [0], # 其中所有节点都是参数节点 + "node_row_ptr": [0, 1, 2], # 用于更快深度优先搜索的行索引 + "heads": [[1, 0, 0]], # 此操作的输出节点的位置 + "attrs": { # 计算图的属性 + "storage_id": ["list_int", [1, 0]], # 存储布局中每个节点的内存插槽 ID + "dtype": ["list_int", [0, 0]], # 每个节点的数据类型(枚举值) + "dltype": ["list_str", [ # 按顺序排列的每个节点的数据类型 + "float32", + "float32"]], + "shape": ["list_shape", [ # 每个节点的 shape 为 k 阶 + [1, 3, 20, 20], + [1, 3, 20, 20]]], + "device_index": ["list_int", [1, 1]], # 按顺序为每个节点分配设备 + } +} +``` + +### 2. 张量转储 + +执行返回的张量是 `tvm.ndarray` 类型。所有张量都将以序列化格式保存为二进制字节。结果二进制字节可以通过 API「load_params」加载。 + +**加载参数示例** + +``` python +with open(path_params, “rb”) as fi: + loaded_params = bytearray(fi.read()) +module.load_params(loaded_params) +``` + +## 如何使用调试器 + +1. 在 `config.cmake` 中把 `USE_PROFILER` 标志设置为 `ON` + + ``` cmake + # 是否开启额外的计算图调试功能 + set(USE_PROFILER ON) + ``` + +2. 执行 `make` tvm,生成 `libtvm_runtime.so` + +3. 在前端脚本文件中导入 `GraphModuleDebug`,`from tvm.contrib.debugger.debug_executor import GraphModuleDebug`,而非 `from tvm.contrib import graph_executor`。 + + ``` python + from tvm.contrib.debugger.debug_executor import GraphModuleDebug + m = GraphModuleDebug( + lib["debug_create"]("default", dev), + [dev], + lib.graph_json, + dump_root="/tmp/tvmdbg", + ) + # 设置输入 + m.set_input('data', tvm.nd.array(data.astype(dtype))) + m.set_input(**params) + # 执行 + m.run() + tvm_out = m.get_output(0, tvm.nd.empty(out_shape, dtype)).numpy() + ``` + +4. 如果之前和共享对象文件/动态链接库一样,**用的是 `lib.export_library("network.so")` 将网络导出到外部库**,调试 runtime 的初始化会略有不同。 + + ``` python + lib = tvm.runtime.load_module("network.so") + m = graph_executor.create(lib["get_graph_json"](), lib, dev, dump_root="/tmp/tvmdbg") + # 设置输入 + m.set_input('data', tvm.nd.array(data.astype(dtype))) + m.set_input(**params) + # 执行 + m.run() + tvm_out = m.get_output(0, tvm.nd.empty(out_shape, dtype)).numpy() + ``` + +输出被转储到 `/tmp` 文件夹中的临时文件夹,或创建 runtime 时指定的文件夹。 + +## 样本输出 + +以下是调试器的输出示例: + +``` bash +Node Name Ops Time(us) Time(%) Start Time End Time Shape Inputs Outputs +--------- --- -------- ------- ---------- -------- ----- ------ ------- +1_NCHW1c fuse___layout_transform___4 56.52 0.02 15:24:44.177475 15:24:44.177534 (1, 1, 224, 224) 1 1 +_contrib_conv2d_nchwc0 fuse__contrib_conv2d_NCHWc 12436.11 3.4 15:24:44.177549 15:24:44.189993 (1, 1, 224, 224, 1) 2 1 +relu0_NCHW8c fuse___layout_transform___broadcast_add_relu___layout_transform__ 4375.43 1.2 15:24:44.190027 15:24:44.194410 (8, 1, 5, 5, 1, 8) 2 1 +_contrib_conv2d_nchwc1 fuse__contrib_conv2d_NCHWc_1 213108.6 58.28 15:24:44.194440 15:24:44.407558 (1, 8, 224, 224, 8) 2 1 +relu1_NCHW8c fuse___layout_transform___broadcast_add_relu___layout_transform__ 2265.57 0.62 15:24:44.407600 15:24:44.409874 (64, 1, 1) 2 1 +_contrib_conv2d_nchwc2 fuse__contrib_conv2d_NCHWc_2 104623.15 28.61 15:24:44.409905 15:24:44.514535 (1, 8, 224, 224, 8) 2 1 +relu2_NCHW2c fuse___layout_transform___broadcast_add_relu___layout_transform___1 2004.77 0.55 15:24:44.514567 15:24:44.516582 (8, 8, 3, 3, 8, 8) 2 1 +_contrib_conv2d_nchwc3 fuse__contrib_conv2d_NCHWc_3 25218.4 6.9 15:24:44.516628 15:24:44.541856 (1, 8, 224, 224, 8) 2 1 +reshape1 fuse___layout_transform___broadcast_add_reshape_transpose_reshape 1554.25 +``` diff --git a/versioned_docs/version-0.12.0/arch/arch/03-virtual_machine.md b/versioned_docs/version-0.12.0/arch/arch/03-virtual_machine.md new file mode 100644 index 00000000..fd1d6b25 --- /dev/null +++ b/versioned_docs/version-0.12.0/arch/arch/03-virtual_machine.md @@ -0,0 +1,301 @@ +--- +title: 向 TVM 中添加虚拟机:Relay 虚拟机 +sidebar_position: 130 +--- + +# 向 TVM 中添加虚拟机:Relay 虚拟机 + +Relay 是一种新的程序表示形式,实现了很多机器学习程序的表示和优化。然而,在支持了一组更具表现力的程序的同时,也引入了几个新的执行挑战。 + +Relay 解释器可以执行完整的语言,但有明显的局限性——它不适合生产部署。它被构造为一个执行 AST 遍历(用来运行程序)的低效解释器。这种方法在概念上很简单,但效率很低,因为 AST 遍历严重依赖于间接层。 + +编译动态代码还存在其他挑战,例如动态调度和分配、完全动态的张量 shape 和控制流。解释器为它们提供了简单的解决方案,但没有一个是完美的。 + +第二种执行机制是已有的图执行器。为将 Relay 程序定位到这里,将它们中的一小部分编译为旧的计算图格式,并在 runtime 上执行。图执行器仅在非常有限的 Relay 程序子集上提供了快速的执行体验。 + +一种非标准的替代方法是 Relay 的 ahead-of-time 编译器,它将 Relay 程序编译为包含提前实现的共享库。ahead-of-time 编译器提供了较好的性能,但难以扩展和检测,只能通过修改代码生成和优化机制来实现。 + +Relay 虚拟机旨在成为一个平衡了这些竞争方法的框架,提供一个动态执行环境——它通过灵活的扩展机制与其他方法(如提前编译)进行扩展、检测和集成。 + +虚拟机是为了取得部署和执行 Relay 程序时性能和灵活性之间的平衡,同时不损失 TVM 的优势。 + +虚拟机(VM)设计是编程语言和系统中一个经过充分研究的领域,成熟的嵌入式编程语言都有多种虚拟机设计。以前的语言虚拟机设计针对传统程序的执行配置文件进行了大量适配。传统程序处理小的标量值,并由大量底层指令组成。 + +由于指令的数量很多,因此指令的执行和调度必须非常高效。机器学习的上下文中,主要用(相对)较少的高级指令来处理张量值。机器学习(ML)程序的 cost 中心是对大量输入的耗时算子的调用,比如 GEMM 或卷积。由于 ML 程序呈现的执行配置文件,标量虚拟机中的微优化显得没那么重要了。 + +TVM 很好地支持了视觉模型,但也希望能够支持更广泛的模型。图执行器能够利用输入图的完全静态特性,来执行积极的优化,比如完全静态分配,以及最佳内存重用。若引入的模型要利用控制流、递归、动态 shapes 和动态分配,就必须改变执行的工作方式。选择 Relay 虚拟机合情合理。 + +本文档的其余部分提供了 Relay 虚拟机设计及其指令集的高级概述。 + +## 设计 + +虚拟机的设计侧重于简单性,同时不牺牲性能。要实现这点,必须专注于设计张量虚拟机,而非标量虚拟机。 + +在张量虚拟机的设置中,进行了以下优化:对象的低成本「分配」(通过避免实际分配)、静态片段的重用,以及进行动态 shape 的能力(即锯齿张量)。 + +### 指令集 + +指令集和指令表示的选择是虚拟机最关键的设计决策。当前的指令表示是包含操作码和数据载荷的标记联合体。重要的设计决策是指令的抽象级别(RISC 与 CISC),以及获取数据的方式(固定宽度指令编码 vs. 可变长度编码)。当前版本更接近于 CISC,具有像 AllocTensor 这样的复杂指令,并且由于包含 shape 作为指令的一部分,因此其长度可变。当前的指令集的级别较高,基本和 Relay 中的高级操作对应。 + +#### Ret + +**参数**: + +```plain +RegName dst +RegName result +``` + +将 `result` 寄存器中的对象返回到调用者的 `dst` 寄存器。 + +#### InvokePacked + +**参数**: + +```plain +Index packed_index +Index arity +Index output_size +RegName* packed_args +``` + +调用 `packed_index` 表示的打包函数。`arity` 和 `output_size` 用于通知虚拟机预期有多少输入和输出。`packed_args` 存储了参数寄存器的列表。注意:`Index` 是 `int64_t` 的别名,在其他指令中也会用到。 + +#### AllocTensor + +**参数**: + +```plain +RegName dst +RegName storage +uint32_t ndim +int64_t* shape +DLDataType dtype +``` + +从给定的存储块 `storage` 分配一个张量值,这个张量值使用常量 shape(存储在 `shape` 中)和 `dtype`。结果保存到 `dst` 寄存器中。 + +#### AllocTensorReg + +**参数**: + +```plain +RegName dst +RegName storage +RegName shape_register +DLDataType dtype +``` + +从给定的存储块(存储在 `storage` 中)分配适当的 shape 的张量值(存储在 `shape_register` 中)和 `dtype`。结果保存到 `dst` 寄存器中。 + +#### AllocStorage + +**参数**: + +```plain +RegName dst +RegName size +RegName alignment +DLDataType dtype_hint +``` + +用给定的 `size`、`alignment` 和数据类型 `dtype_hint` 分配存储块。分配的存储块存储在 `dst` 寄存器中。 + +#### AllocADT + +**参数**: + +```plain +RegName dst +Index tag +Index num_fields +RegName* datatype_fields +``` + +用 `datatype_fields` 寄存器中的 `num_fields` 条目,分配带有 `tag` 标记的数据类型。结果保存到 `dst` 寄存器中。 + +#### AllocClosure + +**参数**: + +```plain +RegName dst +Index clo_index +Index num_freevar +RegName* free_vars; +``` + +用 `clo_index` 的 VMFunction 作为其代码分配一个闭包,并从 `free_vars` 中的寄存器分配 `num_freevar` 条目。结果保存到 `dst` 寄存器中。 + +#### GetField + +**参数**: + +```plain +RegName dst +RegName object +Index field_index +``` + +从 `object` 中获取 `field_index` 索引的字段值。并将结果保存到 `dst` 寄存器中。 + +#### If + +**参数**: + +```plain +RegName test +RegName target +Index true_offset +Index false_offset +``` + +检查 `test` 寄存器中的对象是否等于 `target`。若相等,则通过 `true_offset` 进行相对跳转,否则通过 `false_offset` 进行相对跳转。 + +#### GetTag + +**参数**: + +```plain +RegName object +RegName dst +``` + +获取 `object` 寄存器中 ADT 对象的对象标签。并将结果保存到 `dst` 寄存器中。 + +#### Fatal + +虚拟机执行失败。 + +#### Goto + +**参数**: + +```plain +Index pc_offset +``` + +通过 `pc_offset` 进行无条件相对跳转。 + +#### Invoke + +**参数**: + +```plain +Index func_index +``` + +在 `func_index` 中调用函数,使用 VMFunction 的 arity 字段中包含的参数数量。 + +#### InvokeClosure + +**参数**: + +```plain +RegName closure +Index num_closure_args +RegName* closure_args +``` + +调用 `closure`,使用闭包的 VMFunction 中声明的参数数量。 + +#### LoadConst + +**参数**: + +```plain +RegName dst +Index const_index +``` + +从常量池中加载 `const_index` 处的常量。结果保存到 `dst` 寄存器中。 + +#### LoadConsti + +**参数**: + +```plain +Index val +RegName dst +``` + +将整型常量 `val` 加载到 `dst` 寄存器中。结果是一个秩为 0 的张量。 + +### 对象表示 + +用对象来表示虚拟机使用的对象。 + +目前,`NDArray`、`ADT` 和 `Closure` 这三种类型的对象分别用于表示张量、元组/列表和闭包数据。更多详细信息,可以分别在 [include/tvm/runtime/ndarray.h](https://github.com/apache/tvm/blob/main/include/tvm/runtime/ndarray.h),[include/tvm/runtime/vm/vm.h](https://github.com/apache/tvm/blob/main/include/tvm/runtime/vm/vm.h) 和 [include/tvm/runtime/container.h](https://github.com/apache/tvm/blob/main/include/tvm/runtime/container.h) 中找到。 + +### 堆栈和状态 + +Relay 虚拟机维护一个栈帧(stack frame),其中包含如何恢复之前的调用的信息。寄存器被分配在每个函数的连续空间(虚拟寄存器文件)中。 + +跟踪一组调用的 Relay 函数,一个指向其字节码的指针,以及字节码的偏移量(称为程序计数器)。 + +``` c++ +struct VirtualMachine { + ... + std::vector frames; + ... + // 当前函数。 + size_t func_index; + // 指向当前函数指令的指针。 + const Instruction* code; + // 当前程序计数器相对于代码指针。 + size_t pc; + ... +}; +``` + +### 调度循环 + +虚拟机的一个关键部分是调度循环。调度循环通常主导虚拟机的执行时间,而实验后发现 Relay 并非如此。实现一个简单的 `switch/goto` 调度循环——基于指令操作码进行调度。 + +这个循环由 `VirtualMachine::Run()` 实现。 + +### 虚拟机编译器 + +这个基础架构的一个重要组成部分是将 Relay 的完整 IR 编译成字节码序列的编译器。虚拟机编译器将 `tvm::relay::Module` 转换为 `tvm::relay::vm::Executable`。可执行文件包含一组编译函数(在 `tvm::relay::vm::Function` 中)。这些函数包含有关函数的元数据,及其编译的字节码。可以通过 `tvm::relay::vm::VirtualMachine` 对象加载和运行发出的可执行对象。有关数据结构的完整定义,参见 [include/tvm/runtime/vm/executable.h](https://github.com/apache/tvm/blob/main/include/tvm/runtime/vm/executable.h) 和 [include/tvm/runtime/vm/vm.h](https://github.com/apache/tvm/blob/main/include/tvm/runtime/vm/vm.h)。 + +### 优化 + +虚拟机编译器要进行很多优化。每一个都被实现为 pass,由 Relay pass 管理器管理 。 + +标有 *TODO* 的优化尚未实现。 + +* A-范式 +* Lambda 提升(参见 [src/relay/vm/lambda_lift.cc](https://github.com/apache/tvm/blob/main/src/relay/backend/vm/lambda_lift.cc)) +* 内联原语(参见 [src/relay/vm/inline_primitives.cc](https://github.com/apache/tvm/blob/main/src/relay/backend/vm/inline_primitives.cc)) +* 常量池布局(参见 [src/relay/backend/vm/compiler.cc](https://github.com/apache/tvm/blob/main/src/relay/backend/vm/compiler.cc)) +* 尾调用优化(TODO) +* 存活性分析(TODO) + +### 序列化 + +必须对 Relay 虚拟机编译器生成的可执行文件序列化和反序列化,因为可能要将模型保存到磁盘,然后执行推理。在此之前,Relay 已经在 json 文件中为图执行器生成了一个序列化的表单。但是,相同的格式不能直接用于虚拟机,因为它发出的是字节码,而非计算图样式的程序。可执行文件的序列化本质上需要处理模型特定的(即权重和内核)和虚拟机相关的(即字节码和全局函数名称)数据。 + +对于内核,可以方便地利用现有的 TVM 架构,来保存和加载编译好的库模块。这里只关注用二进制格式来序列化其他几个组件,这些组件按以下顺序组织: + +* 全局部分。这一节包含虚拟机使用的全局变量(函数名称)。 +* 常量部分。这一节用于存储虚拟机的常量池(即模型的权重)。 +* 原语名称部分。引入这一节是为了归纳由虚拟机调用的原语算子名称列表,即以 `fused_` 开头的名称。原语名称用作符号,从而在编译的内核库中查找函数指针。 +* 代码部分。包括字节码在内的虚拟机函数位于这一节中。调度循环遍历此部分以获取执行指令。 + +因此,不同于包含权重(.params)、图 json(.json)和编译内核库(.so)的图执行器 artifact,序列化的可执行 artifact 由 Relay 对象文件(.ro)和编译内核组成(.so)。 + +实现的 `save` 函数将可执行文件存储到磁盘,并序列化为上述格式。同时,`load_exec` 函数用于加载序列化的内核二进制以及可执行相关的二进制代码,这些二进制代码之后也会用于实例化虚拟机对象。更多示例,参阅 [test_vm_serialization.py](https://github.com/apache/tvm/blob/main/tests/python/relay/test_vm_serialization.py) 文件。 + +### 未解决的问题 + +#### 如何处理动态 shape? + +随着 Relay(TVM 的编译器)的升级,TVM 对动态 shape 的支持也在不断发展。推荐在 TVM 的论坛(https://discuss.tvm.apache.org/)中获取有关动态 shape 支持的最新进展。 + +#### 如何修改虚拟机来支持某些代码路径的 JIT 编译? + +在代码生成空间中,仍有许多权衡因素需要分析。虚拟机的设计非常灵活,因此可以对其进行修改,供将来的实验使用。 + +#### 如何支持异构执行? + +假设已经对合适的设备副本进行了注解,异构执行应该可以开箱即用。为正确执行此操作,要运行设备注解和拷贝 pass。 diff --git a/versioned_docs/version-0.12.0/arch/arch/04-introduction_to_module_serialization.md b/versioned_docs/version-0.12.0/arch/arch/04-introduction_to_module_serialization.md new file mode 100644 index 00000000..0b36d6d1 --- /dev/null +++ b/versioned_docs/version-0.12.0/arch/arch/04-introduction_to_module_serialization.md @@ -0,0 +1,154 @@ +--- +title: 模块序列化简介 +sidebar_position: 140 +--- + +# 模块序列化简介 + +当部署 TVM runtime 模块的时候,无论是 CPU 还是 GPU,TVM 只需要一个动态共享库。关键是统一的模块序列化机制。本节将介绍 TVM 模块序列化的格式标准和实现细节。 + +## 模块导出示例 + +首先,为 GPU 构建一个 ResNet-18 工作负载,以此示例: + +``` python +from tvm import relay +from tvm.relay import testing +from tvm.contrib import utils +import tvm + +# Resnet18 工作负载 +resnet18_mod, resnet18_params = relay.testing.resnet.get_workload(num_layers=18) + +# 构建 +with relay.build_config(opt_level=3): + _, resnet18_lib, _ = relay.build_module.build(resnet18_mod, "cuda", params=resnet18_params) + +# # 创建一个临时目录 +temp = utils.tempdir() + +# 路径库 +file_name = "deploy.so" +path_lib = temp.relpath(file_name) + +# 导出库 +resnet18_lib.export_library(path_lib) + +# 加载回来 +loaded_lib = tvm.runtime.load_module(path_lib) +assert loaded_lib.type_key == "library" +assert loaded_lib.imported_modules[0].type_key == "cuda" +``` + +## 序列化 + +入口 API 是 `tvm.module.Module` 的 `export_library`。函数将执行以下步骤: + +1. 收集所有 DSO 模块(LLVM 模块和 C 模块) +2. 有了 DSO 模块后,调用 `save` 函数,并将其保存至文件。 +3. 接下来,检查模块是否导入,例如 CUDA、OpenCL 或其他任何模块。这里不限制模块类型。导入模块后,创建一个名为 `devc.o` / `dev.cc` 的文件(以便可将导入模块的二进制 blob 数据嵌入到一个动态共享库中),然后调用 `_PackImportsToLLVM` 或 `_PackImportsToC` 函数进行模块序列化。 +4. 最后,调用 `fcompile`,它会调用 `_cc.create_shared` 来获取动态共享库。 + +:::note +1. 对于 C 源代码模块,先编译,然后将它们与 DSO 模块链接。 +2. 用 `_PackImportsToLLVM` 还是 `_PackImportsToC` 取决于 TVM 是否启用 LLVM。它们的目的其实是一样的。 +::: + +## 序列化和格式标准的底层 + +如前所述,序列化工作将在 `_PackImportsToLLVM` 或 `_PackImportsToC` 中进行。它们都调用 `SerializeModule` 来序列化 runtime 模块。在 `SerializeModule` 函数中,首先构造一个辅助类 `ModuleSerializer`。`module` 要做一些初始化工作,比如标记模块索引。然后可以用它的 `SerializeModule` 来序列化模块。 + +为了方便大家理解,接下来我们将详细讲解这个类的实现。 + +以下代码用于构造 `ModuleSerializer`: + +``` c++ +explicit ModuleSerializer(runtime::Module mod) : mod_(mod) { + Init(); +} +private: +void Init() { + CreateModuleIndex(); + CreateImportTree(); +} +``` + +在 `CreateModuleIndex()` 中,会用 DFS 检查模块导入关系,并为它们创建索引。注意,根模块固定在位置 0。示例中,模块关系如下: + +``` c++ +llvm_mod:imported_modules + - cuda_mod +``` + +因此,LLVM 模块的索引为 0,CUDA 模块的索引为 1。 + +模块索引构建后,用 `CreateImportTree()` 来构建导入树(import tree),其作用是在加载导出的库时,恢复模块导入关系。在我们的设计中,用 CSR 格式存储导入树,每一行都是父索引,子索引(child indices)对应它的孩子索引(children index)。代码中用 `import_tree_row_ptr_` 和 `import_tree_child_indices_` 来表示它们。 + +初始化后,可以用 `SerializeModule` 函数来序列化模块。在其功能逻辑中,假设序列化格式如下: + +``` text +binary_blob_size +binary_blob_type_key +binary_blob_logic +binary_blob_type_key +binary_blob_logic +... +_import_tree +_import_tree_logic +``` + +`binary_blob_size` 是这个序列化步骤中的 blob 数量。示例中,分别为 LLVM 模块、CUDA 模块和 `_import_tree` 创建了三个 blob。 + +`binary_blob_type_key` 是模块的 blob 类型键。对于 LLVM/C 模块,其 blob 类型键为 `_lib`。对于 CUDA 模块,则是 `cuda`,可以通过 `module->type_key()` 获取。 + +`binary_blob_logic` 是 blob 的逻辑处理。对于大多数 blob(如 CUDA、OpenCL),可以调用 `SaveToBinary` 函数来将 blob 序列化为二进制。但是,类似 LLVM/C 模块,写成 `_lib` 表示这是一个 DSO 模块。 + +:::note +是否需要实现 SaveToBinary 虚函数,取决于模块的使用方式。例如,加载动态共享库时,若模块有我们需要的信息,则应该实现 SaveToBinary 虚函数。它类似于 CUDA 模块,加载动态共享库时,要将其二进制数据传递给 GPU 驱动程序,因此实现 `SaveToBinary` 来序列化其二进制数据。但是对于主机模块(如 DSO),加载动态共享库时不需要其他信息,因此无需实现`SaveToBinary`。但是,若之后要记录一些 DSO 模块的元信息,也可以为 DSO 模块实现 `SaveToBinary`。 +::: + +最后,除非模块只有一个 DSO 模块,并且在根目录下,否则要编写一个主要的 `_import_tree`。如前所述,它用于将导出的库加载回来时,重建模块导入关系。`import_tree_logic` 只是将 `import_tree_row_ptr_` 和 `import_tree_child_indices_` 写入流(stream)中。 + +完成这一步后,将其打包到符号 `runtime::symbol::tvm_dev_mblob` 中,这个符号可以在动态库中恢复。 + +至此,完成了序列化部分。可以看到,现在已经可以理想地支持任意模块的导入了。 + +## 反序列化 + +入口 API 是 `tvm.runtime.load`。这个函数本质上的作用就是调用 `_LoadFromFile`,更具体一点,就是 `Module::LoadFromFile`。示例中,文件是 `deploy.so`,根据函数逻辑,将调用 `dso_library.cc` 中的 `module.loadfile_so`。关键点如下: + +``` c++ +// 加载导入的模块 +const char* dev_mblob = reinterpret_cast(lib->GetSymbol(runtime::symbol::tvm_dev_mblob)); +Module root_mod; +if (dev_mblob != nullptr) { + root_mod = ProcessModuleBlob(dev_mblob, lib); +} else { + // 只有一个 DSO 模块 + root_mod = Module(n); +} +``` + +如前所述,blob 会被打包到符号 `runtime::symbol::tvm_dev_mblob` 中。可以在反序列化部分对其进行检查。若有 `runtime::symbol::tvm_dev_mblob`,则调用 `ProcessModuleBlob`,逻辑如下: + +``` c++ +READ(blob_size) +READ(blob_type_key) +for (size_t i = 0; i < blob_size; i++) { + if (blob_type_key == "_lib") { + // 用 lib 构建 dso 模块 + } else if (blob_type_key == "_import_tree") { + // READ(_import_tree_row_ptr) + // READ(_import_tree_child_indices) + } else { + // 调用 module.loadbinary_blob_type_key,如module.loadbinary_cuda 来恢复。 + } +} +// 用 _import_tree_row_ptr 和 _import_tree_child_indices 来恢复模块导入关系。 +// 根据之前说的不变性,第一个模块是根模块。 +return root_module; +``` + +之后,将 `ctx_address` 设置为 `root_module`,使得可以从根目录查找符号(因此所有符号都是可见的)。 + +至此,完成了反序列化部分。 diff --git a/versioned_docs/version-0.12.0/arch/arch/05-device_target_interactions.md b/versioned_docs/version-0.12.0/arch/arch/05-device_target_interactions.md new file mode 100644 index 00000000..fc4c862f --- /dev/null +++ b/versioned_docs/version-0.12.0/arch/arch/05-device_target_interactions.md @@ -0,0 +1,78 @@ +--- +title: 设备/Target 交互 +sidebar_position: 150 +--- + +# 设备/Target 交互 + +本文档适用于有兴趣了解 TVM 框架如何与特定设备 API 交互的开发者,或希望实现对新 API 或新硬件的支持的开发者。 + +所有新的 runtime 环境都必须实现的三个主要方面: + +* [DeviceAPI](#tvm-target-specific-device-api) 类为特定设备提供句柄,以及用于与其交互的 API。它定义了一个通用接口,用于查询设备参数(例如可用内存、线程数等)和执行简单操作(例如,从主机复制内存,或在设备上的缓冲区之间复制)。 +* [Target](#tvm-target-specific-target) 类描述了运行函数的设备。它既对 target 代码生成器公开,也对优化 pass 公开。 +* [target 代码生成器](#tvm-target-specific-codegen) 从 IRModule 构造了一个 [模块](/docs/arch/arch/runtimes#module),它由一个或多个 [PackedFunc](/docs/arch/arch/runtimes#PackedFunc) 组成。 + +## DeviceAPI + +`DeviceAPI` 表示特定硬件设备 API 的句柄。(例如,`CUDADeviceAPI` 通过 CUDA 框架处理所有的交互。)大多数 `DeviceAPI` 方法接收一个 `device_id` 参数,来指定访问哪个设备。Python 中通常用 `tvm.runtime.device()` 函数访问它们,这个函数返回特定设备的句柄,通过特定 API 访问。(例如,`tvm.runtime.device()` 通过 CUDA API 访问物理设备 `0`。) + +* 属性查询 - `GetAttr` 允许查询不同的设备特定的参数,例如设备名称、线程数等。[device_api.h](https://github.com/apache/tvm/blob/main/include/tvm/runtime/device_api.h) 中的 `enum DeviceAttrKind` 定义了可以查询的参数。有些设备不支持部分可查询参数。若无法查询某个参数(例如 Vulkan 上的 `kMaxClockRate`),或者某个参数不适用(例如 CPU 上的 `kWarpSize`),那么查询返回 `nullptr`。 +* 设置活动设备 - `SetDevice` 应将某个特定设备设为活动设备。若要在设备上执行 target 特定的 codegen 生成的 `PackedFunc` ,应该在活动设备上运行。 +* 内存管理 - 用于在设备上分配和释放内存的程序。 + * 分配数据空间 - `AllocDataSpace` 和 `FreeDataSpace` 在设备上分配和释放空间。这些分配可以作为输入和输出提供给算子,并构成算子计算图的主要数据流。它们必须能够在主机和数据空间之间传输数据。返回值是一个不透明的 `void*`。虽然某些实现返回一个内存地址,但这不是必需的,并且 `void*` 可能是不透明句柄,只能由生成它的设备后端解释。 `void*` 用作其他后端特定的函数的参数,例如 `CopyDataFromTo`。 + * 分配工作空间 - `AllocWorkspace` 和 `FreeWorkspace` 在设备上分配和释放空间。不同于数据空间,它们用于存储算子定义中的中间值,并且不需要传输到主机设备,或从主机设备传输。若 `DeviceAPI` 子类没有实现这些方法,它们会默认调用相应的 `DataSpace` 函数。 + * 复制数据 - `CopyDataFromTo` 应该将数据从一个位置复制到另一个位置。副本的类型由 `dev_from` 和 `dev_to` 参数确定。实现应支持在单个设备上将内存从 CPU 复制到设备、从设备复制到 CPU,以及从一个缓冲区复制到另一个缓冲区。若源位置或目标位置在 CPU 上,则对应的 `void*` 指向一个 CPU 地址,这个地址可以传递给 `memcpy`。若源位置或目标位置在设备上,则相应的 `void*` 之前已由 `AllocDataSpace` 或 `AllocWorkspace` 生成。 + + 这些副本排队等待在特定的 `TVMStreamHandle` 上执行。但是,该实现不应该假定在 `CopyDataFromTo` 调用完成后,CPU 缓冲区仍然有效或可访问。 +* 执行流管理 - 用于处理 `TVMStreamHandle` 的程序,它表示用于执行命令的并行执行流。 + * 创建流 - `CreateStream` 和 `FreeStream` 为执行流分配/释放句柄。若设备仅实现一个命令队列,则 `CreateStream` 返回 `nullptr`。 + * 设置活动流 - `SetStream` 将流设置为活动的。在活动期间,若特定于 target 的 code gen 生成的 `PackedFunc` 需要在设备上执行,则应将工作提交到活动流。 + * 同步到 CPU - `StreamSync` 将执行流同步到 CPU。`StreamSync` 调用一次性返回所有的内存转换,以及在调用完成前提交的计算。 + * 在流之间同步 - `SyncStreamFromTo` 在源流和目标流之间引入同步屏障(synchronization barrier)。即在源流完成当前排队的所有命令前,目标流不会超出当前排队的命令。 + +TVM 框架若要使用新的 DeviceAPI,应该按照以下步骤注册: + +1. 创建一个函数,它实例化新 DeviceAPI,并返回一个指向它的指针: + + ``` c++ + FooDeviceAPI* FooDeviceAPI::Global() { + static FooDeviceAPI inst; + return &inst; + } + ``` + +2. 将函数注册到 TVM 注册表: + + ``` c++ + TVM_REGISTER_GLOBAL("device_api.foo").set_body_typed(FooDeviceAPI::Global); + ``` + +1. 为新的 DeviceAPI 添加一个进入 [c_runtime_api.h](https://github.com/apache/tvm/blob/main/include/tvm/runtime/c_runtime_api.h) 中的 `TVMDeviceExtType` 枚举的入口。该值是一个未使用值,它大于 `DLDeviceType::kDLExtDev`,但小于 `DeviceAPIManager::kMaxDeviceAPI`。 +2. 给 [device_api.h](https://github.com/apache/tvm/blob/main/include/tvm/runtime/device_api.h) 中的 `DeviceName` 添加一个案例,从而将枚举值转换为字符串表示形式。这个字符串表示应该和 `TVM_REGISTER_GLOBAL` 的名称匹配。 +3. 将入口添加到 `tvm.runtime.Device` 的 `MASK2STR` 和 `STR2MASK` 字典,获取新的枚举值。 + +## Target 定义 + +`Target` 对象是属性(包含物理设备、其硬件/驱动程序限制,及其功能)的查找表。在优化和代码生成阶段都可以访问 `Target`。虽然相同的 `Target` 类适用于所有 runtime target,但每个 runtime target 可能需要添加特定于 target 的选项。 + +在 [target_kind.cc](https://github.com/apache/tvm/blob/main/src/target/target_kind.cc) 中,为 `TVM_REGISTER_TARGET_KIND` 添加一个新的声明,传递新 target 的字符串名称,以及该 target 运行设备的 `TVMDeviceExtType` 或 `DLDeviceType` 枚举值。通常,target 名称和设备名称匹配。(例如,`"cuda"` target 在 `kDLCUDA` 设备上运行。)但也有例外,例如多个不同的代码生成 targets 可以在同一个物理设备上运行。 (例如,`"llvm"` 和 `"c"` targets 都在 `kDLCPU` 设备类型上运行。) + +特定 target 种类的所有选项都使用 `add_attr_option` 函数添加,具有可选的默认值。可以用 `set_target_parser` 来添加 *Target* 解析器,来处理那些动态基于其他参数,或是从设备属性查询到的参数。 + +这个参数定义了一个解析器,可以解析 target 的字符串描述。这是在 C++ 的 `Target::Target(const String&)` 构造函数中实现的,它接收 JSON 格式的字符串,通常用 `tvm.target.Target` 这个 Python 对象来调用。例如, `tvm.target.Target('{"kind": "cuda", "max_num_threads": 1024}')` 会创建一个 `cuda` target,同时覆盖默认的最大线程数。 + +代码生成器中可以用 C++ 中的 `target->GetAttr(param_name)`,或是 Python 中的 `target.attrs` 字典来访问 target 属性。 + +## Target 代码生成器 + +代码生成器采用优化的 `IRModule`,并将其转换为可执行表示。每个代码生成器注册后,才能被 TVM 框架使用。这是通过注册 `"target.build.foo"` 函数来完成的,其中 `foo` 与上面的 `TVM_REGISTER_TARGET_KIND` 定义中使用的名称相同。 + +``` c++ +tvm::runtime::Module GeneratorFooCode(IRModule mod, Target target); +TVM_REGISTER_GLOBAL("target.build.foo").set_body_typed(GeneratorFooCode); +``` + +代码生成器有两个参数:第一个是要编译的 `IRModule`,第二个是描述代码要在哪个设备运行的 `Target`参数。因为执行编译的环境与执行代码的环境不一定相同,所以代码生成器不应该在设备本身上执行任何属性的查找,而是应该访问存储在 `Target` 中的参数。 + +输入 `IRModule` 中的每个函数都可以在输出 `runtime::Module` 中按名称访问。 diff --git a/versioned_docs/version-0.12.0/arch/arch/06-pass_infra.md b/versioned_docs/version-0.12.0/arch/arch/06-pass_infra.md new file mode 100644 index 00000000..ee55fcea --- /dev/null +++ b/versioned_docs/version-0.12.0/arch/arch/06-pass_infra.md @@ -0,0 +1,493 @@ +--- +title: Pass Infrastructure +sidebar_position: 160 +--- + +# Pass Infrastructure + +Relay 和 TVM IR 都包含了一系列优化 pass,它们可提高模型的性能指标,如平均推理、内存占用或特定设备的功耗。有一套标准优化以及机器学习特定的优化,包括常量折叠、死代码消除、算子布局更改、算子融合、buffer 处理和循环转换等。每个 pass 都被构造为 ir-to-ir 转换,它们用遍历期间和/或遍历之前收集的分析结果进行转换。 + +TVM 的快速发展催生更加系统化和有效的方式,来管理这些 pass。此外,管理 TVM 堆栈不同层(例如 Relay 和 tir)pass 的通用框架极大便利了开发者,使他们可以快速原型化并将实现的 pass 插入至系统。 + +该文档描述了一个 infra 的设计——利用生产编译器来管理优化 pass 的方式,以及用于构建层的现代深度学习框架的风格。 + +例如,许多现有的生产编译器,如 GCC 和 LLVM,都使用 pass 管理器来有效地管理 pass 的执行。最初管理 pass 很简单,因为 pass 的数量很少,但成熟的编译器包含数百个单独的 pass。外部用户通常希望正确调度自定义 pass,而无需修改单个手工制作的 pass 顺序。 + +类似地,现代深度学习框架,如 Pytorch 和 MXNet Gluon,也倾向于分别通过 [Sequential](https://pytorch.org/docs/stable/nn.html?highlight=sequential#torch.nn.Sequential) 和 [Block](https://mxnet.apache.org/api/python/docs/api/gluon/block.html#gluon-block) 实现 pass 模式的层构建方案。得到了这样的结构后,这些现代框架能够方便地将模块/层添加到容器中,并轻松地构建神经网络。 + +Relay pass infra 的设计,很大程度上是受到 LLVM 中使用的分层 pass 管理器,以及流行的深度学习框架中使用的块式容器的启发。Pass infra 的主要目标: + +1. 实现更好的优化程序编排。用户可以灵活地定制和构建自己的优化 pipeline。 +2. 提供一种对用户更友好的方式来调试优化 pass。 +3. 减轻开发者手动解决 pass 之间的依赖关系的工作量。 +4. 为开发者简化新 pass 的实现。允许用户在 Python 中实现 pass,并让 pass 基础架构操纵其执行。 + +## 设计 + +聚焦用户扩展的简易性,使用户可以快速添加新的 pass,且向后兼容。设计包含后端和前端。前者(即后端)实现了 pass infra 的主要逻辑。后者(即前端)为用户提供简单的 API 进行交互,允许用户快速创建自己的优化 pipeline。 + +### C++ 后端 + +`PassInfo` 对象包含一个 pass 所需的基本信息,其中 `name` 是 pass 名称,`opt_level` 表示将在哪个优化级别启用 pass,`required` 表示执行某个 pass 所需的 pass(有关详细信息,参阅 [include/tvm/ir/transform.h](https://github.com/apache/tvm/blob/main/include/tvm/ir/transform.h))。例如,在注册 pass 期间(稍后介绍),pass 开发者可以指定 pass 的名称、执行的优化级别和/或所需的 pass。`opt_level` 可帮助 pass infra 识别在用户提供的优化级别下运行时,是否需要执行某个 pass。pass infra 可以用 `required` 字段来解决 pass 依赖。 + +``` c++ +class PassInfoNode : public Object { + String name; + int opt_level; + Array required; +}; +``` + +#### PassContext + +`PassContext` 具备优化 pass 的有用信息。例如,它包含错误报告系统,可以提供优化失败原因的诊断。 `PassContext` 用来替换旧的 `BuildConfig`,`BuildConfig` 用于帮助用户配置编译选项,包括优化级别和必需/禁用的 pass 等。例如,有一个配置,在 `opt_level=3` 执行所有 pass ,同时用 `PassContext` 提供的 `disabled_pass=xx` 来禁用一些 pass 。在 `opt_level=3` 处全局化所有 pass ,并排除禁用 pass 列表中的所有 pass。`PassContext` 还提供了一种检测所有 pass 的方法。参阅 [Pass Instrument](https://tvm.apache.org/docs/arch/pass_infra.html#pass-instrument-cpp-backend) 这一节。 + +用户可以用这个类编写 Python `with` 语法,从而在一定的配置下进行优化。此外,用户可以通过 `PassContext::Current()` 以线程安全的方式获取某个程序范围内可用的上下文,因为线程本地存储的 `PassContextThreadLocalStore` 用于保存创建的 pass 上下文对象。稍后用示例来展示如何用 C++ 和 Python API 来通过 pass 上下文创建编译 pipeline。 + +``` c++ +class PassContextNode : public Object { + public: + int opt_level{2}; + tvm::Array required_pass; + tvm::Array disabled_pass; + mutable Optional diag_ctx; + Map config; + Array instruments; +}; + +class PassContext : public NodeRef { + public: + TVM_DLL static PassContext Create(); + TVM_DLL static PassContext Current(); + TVM_DLL void InstrumentEnterPassContext(); + TVM_DLL void InstrumentExitPassContext(); + TVM_DLL bool InstrumentBeforePass(const IRModule& mod, const PassInfo& info) const; + TVM_DLL void InstrumentAfterPass(const IRModule& mod, const PassInfo& info) const; + /* 省略其他字段。 */ + + private: + // pass 上下文范围的入口。 + TVM_DLL void EnterWithScope(); + // pass 上下文范围的退出。 + TVM_DLL void ExitWithScope(); + + // 获取 Python `with` 语法的类。 + friend class tvm::With; +}; + +struct PassContextThreadLocalEntry { + /*! \摘要:默认 pass 上下文。 */ + PassContext default_context; + /*! \摘要:当前 pass 上下文 */ + std::stack context_stack; + PassContextThreadLocalEntry() { + default_context = PassContext(make_node()); + } +}; + +/*! 保存 pass 上下文的线程本地存储 */ +typedef dmlc::ThreadLocalStore + PassContextThreadLocalStore; +``` + +#### Pass 构造 + +pass infra 以分层的方式设计,可以在不同粒度的 Relay/tir 程序下工作。我们引入一个纯虚类 `PassNode`,作为不同优化 pass 的基础。这个类包含几个必须由子类在模块、函数或 pass 序列级别实现的虚拟方法。 + +``` c++ +class PassNode : Object { + virtual PassInfo Info() const = 0; + virtual Module operator()(const IRModule& mod + const PassContext& pass_ctx) const = 0; +}; +``` + +仿函数展示了 pass 是如何实现的,即它始终在特定上下文对 `IRModule` 起作用。所有 pass 都以 `Module` 到 `Module` 的方式设计。因此,由 pass infra 管理的优化将始终更新整个模块。 + +已经创建了几个子类来实现不同类型的优化 pass,例如,函数级 pass、模块级 pass 和顺序 pass。每个子类本身都可以充当 pass 管理器。例如,它们可以收集所需的 pass 并执行,或是基于给定的元数据构建依赖关系图。访问 [src/relay/ir/transform.cc](https://github.com/apache/tvm/blob/main/src/relay/ir/transform.cc) 和 [src/ir/transform.cc](https://github.com/apache/tvm/blob/main/src/ir/transform.cc),查看完整定义。 + +#### 模块级 Pass + +模块级 pass 主要针对全局和过程间优化(IPO),类似于 LLVM 中的模块 pass。Relay 中一些需要模块全局图的典型 pass,如 A-normal form 转换和 lambda 提升等,都属于这个集合。在这个级别,用户甚至可以在模块中添加和/或删除功能。注意,是所有 pass。 + +``` c++ +class ModulePassNode : PassNode { + PassInfo pass_info; + runtime::TypedPackedFunc pass_func; + Module operator()(const Module& mod, const PassContext& pass_ctx) const final; + // 其他成员/方法省略 +}; +``` + +`pass_info` 维护模块级 pass 所需的信息。`pass_func` 描述真正的优化。例如,可能需要消除模块的死代码。可在 `pass_func` 中实现算法,并让它在模块上运行。然后它将删除死代码,包括模块中未使用的函数。注意,该字段被设计为一个打包函数,可以在 C++ 和 Python 中实现优化。 + +#### 函数级 Pass + +函数级 pass 用于对给定的 Relay/tir 模块进行各种函数内的优化。它每次从模块的函数列表中获取一个函数进行优化,并产生一个重写的 Relay `Function` 或 tir `PrimFunc`。大部分 pass 都可以归为这一类,比如 Relay 中常见的子表达式消除和推理简化,以及 tir 中的向量化和展平存储等。 + +注意,这个级别的 pass 范围是 Relay 函数或 tir 原始函数。因为它们不知道全局信息,所以无法通过这些 pass 添加或删除功能。 + +``` c++ +class FunctionPassNode : PassNode { + PassInfo pass_info; + runtime::TypedPackedFunc pass_func; + Module operator()(const Module& mod, const PassContext& pass_ctx) const final; + bool SkipFunction(const Function& func) const; + // 其他成员/方法省略... +}; +``` + +`pass_info` 与模块 pass 中的描述相同。`pass_func` 接收一个函数进行优化,还需要一个模块,可以使用模块来报错。用「SkipOptimization」注解函数,从而在优化期间将忽略它。 + +#### 顺序 Pass + +`SequentialPass` 类似于 Pytorch 中 `nn.Sequential`,包含许多用于执行的 pass。 + +``` c++ +class SequentialPassNode : PassNode { + PassInfo pass_info; + // 要执行的 Pass。 + Array passes; + bool PassEnabled(const PassInfo& info) const; + Module operator()(const Module& mod, const PassContext& pass_ctx) const final; +}; +``` + +目前在 Relay 中只有少数 pass 被放入该组。例如,`FoldScaleAxis` 需要在内部调度 `ForwardFoldScaleAxis` 和 `BackwardFoldScaleAxis`。另外,推荐先实现 `BackwardFoldScaleAxis`。因此,这个 pass 是 `SequentialPass` 的理想候选。 + +以下代码展示了如何调用顺序 pass 中的各个 pass。本质上,按照它们在 pass 列表中的顺序依次执行每个 pass。 + +``` c++ +Module SequentialNode::operator()(const Module& module, + const PassContext& pass_ctx) const { + Module mod = module; + for (const Pass& pass : passes) { + ICHECK(pass.defined()) << "Found undefined pass for optimization."; + const PassInfo& pass_info = pass->Info(); + if (!PassEnabled(pass_info)) continue; + for (const auto& it : pass_info->required) { + const auto* name = it.as(); + ICHECK(name); + mod = GetPass(name->value)(mod, pass_ctx); + } + mod = pass(mod, pass_ctx); + } + return mod; +} +``` + +调用 pass 首先会检查这个 pass 是否启用——首先检查 pass 是否被用户明确禁用,然后检查它是否被用户指定为必需 pass。如果仍不能确定,则检查其 `opt_level`。只有当它的优化级别不低于在 pass 上下文中配置的优化级别时,才会启用并执行此 pass。 + +要执行 pass,首先要用 pass 名称在 TVM 打包函数注册表中检索已注册的 pass。每个 pass 都注册了一个 API 端点,将在后面展示。 + +``` c++ +Pass GetPass(const std::string& pass_name) { + using tvm::runtime::Registry; + std::string fpass_name = "relay._transform." + pass_name; + const auto* f = Registry::Get(fpass_name); + ICHECK(f != nullptr) << "Cannot find " << fpass_name + << "to create the pass " << pass_name; + return (*f)(); +} +``` + +一些辅助函数可以创建上述这些 pass 的每种类型。这些辅助函数也提供给 Python 前端,以便用户更好地使用 Python API 来创建特定的 pass 对象。 + +``` c++ +Pass CreateFunctionPass( + const runtime::TypedPackedFunc& pass_func, + int opt_level, + String name, + Array required); + +Pass CreatePrimFuncPass( + const runtime::TypedPackedFunc& pass_func, + int opt_level, + String name, + Array required); + +Pass CreateModulePass( + const runtime::TypedPackedFunc& pass_func, + int opt_level, + String name, + Array required); + +Pass Sequential(tvm::Array passes, PassInfo pass_info); +``` + +#### Pass 注册 + +前面已经介绍了不同级别 pass 的概念和用于编译的上下文。接下来以常量折叠为例,介绍用户注册 pass。这个 pass 可以在 Relay 函数(在 [src/relay/transforms/fold_constant.cc](https://github.com/apache/tvm/blob/main/src/relay/transforms/fold_constant.cc) 中)中折叠常量。 + +提供一个 API 来执行 `Expr` 到 `Expr` 的转换。 + +``` c++ +Expr FoldConstant(const Expr& expr); +``` + +为了将这个 pass 注册到 pass infra,首先决定这个 pass 要在哪个级别执行。由于常量折叠发生在单个函数上,应该通过 `CreateFunctionPass` 直观地为它创建一个 `FunctionPass`。`pass_func` 作为一个打包函数返回,该函数在 *IRModule* 中的每个函数上调用 `Expr` 到 `Expr` API。`{}` 表示此 pass 不需要任何先决条件。否则,pass 开发者必须识别并列出它们。 + +同时,通过名称 `relay._transform.FoldConstant` 注册了一个 pass API 端点。因此,此 pass 成为注册表中的一个条目,可以在需要时由 C++(例如上面的 `GetPass`)和 Python 访问。 + +``` c++ +namespace transform { + +Pass FoldConstant() { + runtime::TypedPackedFunc pass_func = + [=](Function f, IRModule m, PassContext pc) { + return Downcast(FoldConstant(f)); + }; + return CreateFunctionPass(pass_func, 2, "FoldConstant", {}); +} + +TVM_REGISTER_GLOBAL("relay._transform.FoldConstant") +.set_body_typed(FoldConstant); + +} // 命名空间变换 +``` + +为了允许其他 C++ 模块应用此 pass,在 [include/tvm/relay/transform.h](https://github.com/apache/tvm/blob/main/include/tvm/relay/transform.h) 中声明了一个自由函数,如下所示: + +``` c++ +TVM_DLL Pass FoldConstant(); +``` + +#### Pass Instrument + +Pass Instrument 是一种分析 pass 本身的机制。例如,可以用基础架构来了解 pass 需要多少时间和内存,或者 pass 如何转换 IR 模块。 + +在 `PassContext` 的生命周期中引入了四个检测点。 + +``` c++ +TVM_DLL void InstrumentEnterPassContext(); +TVM_DLL void InstrumentExitPassContext(); +TVM_DLL bool InstrumentBeforePass(const IRModule& mod, const PassInfo& info) const; +TVM_DLL void InstrumentAfterPass(const IRModule& mod, const PassInfo& info) const; +``` + +进入 `PassContext` 实例的范围时,立即调用 `InstrumentEnterPassContext`。 + +离开了 `PassContext` 的范围或者 pass 执行过程中发生了异常,会调用 `InstrumentExitPassContext`。当工具类被 `tvm.transform.PassContext` 中的 `override_instruments` 覆盖时,也会调用此方法。参阅 [在当前 PassContext 中复写工具类](https://tvm.apache.org/docs/arch/pass_infra.html#pass-instrument-overriden)。 + +`InstrumentBeforePass` 在执行前被调用。如果要在执行后运行 pass,则调用 `InstrumentAfterPass`。行为如下: + +``` c++ +if (pass_ctx.InstrumentBeforePass(ir_module, pass_info)) { + new_ir_module = run_pass(ir_module, pass_ctx); + pass_ctx.InstrumentAfterPass(new_ir_module, pass_info); + return new_ir_module; +} +``` + +`PassInstrument` 接口允许在上述四种方法中运行任意代码。多个 `PassInstrument` 实例可以注册到一个 `PassContext` 中。`PassInstrument` 实例按照传递给 `PassContext` 的 `instruments` 参数的顺序依次调用。 + +`PassInstrument` 提供以下接口: + +``` c++ +namespace instrument { + +class PassInstrumentNode : public Object { + public: + String name; + virtual void EnterPassContext() const = 0; + virtual void ExitPassContext() const = 0; + virtual bool ShouldRun(const IRModule& mod, const transform::PassInfo& info) const = 0; + virtual void RunBeforePass(const IRModule& mod, const transform::PassInfo& info) const = 0; + virtual void RunAfterPass(const IRModule& mod, const transform::PassInfo& info) const = 0; + /* 省略其他字段。 */ +}; + +class PassInstrument : public ObjectRef { + public: + TVM_DEFINE_OBJECT_REF_METHODS(PassInstrument, ObjectRef, PassInstrumentNode); +}; + +} // 命名空间工具 +``` + +提供 Python 前端快速实现 `PassInstrument`。参阅 [Pass 工具](https://tvm.apache.org/docs/arch/pass_infra.html#pass-instrument-py-frontend)。 + +在 `PassContext` 中,`PassInstrument` 实例的调用顺序如下: + +``` python +with PassContext(instruments=[pi]) # pi = a PassInstrument implementation. # pi = PassInstrument 实现。 + pi.EnterPassContext() + + if pi.ShouldRun(Pass1): + pi.RunBeforePass() + Pass1() + pi.RunAfterPass() + + if pi.ShouldRun(Pass2): + pi.RunBeforePass() + Pass2() + pi.RunAfterPass() + + pi.ExitPassContext() +``` + +接下来简单介绍 `PassInstrument` 接口和 `PassContext` 方法的关系。更多详细信息,参阅([src/ir/transform.cc](https://github.com/apache/tvm/blob/main/src/ir/transform.cc))。 + +* `InstrumentEnterPassContext` + * `EnterPassContext()` 按照传递给 `PassContext` 的 `instruments` 的顺序执行。 + * 当抛出异常时,`PassContext` 通过清除所有已注册的 `PassInstrument` 实例来禁用 pass 工具。 + * 然后 `PassContext` 执行每个成功完成 `EnterPassContext()` 的 `PassInstrument` 实例的 `ExitPassContext()` 方法 + * 例如,如果将 `PassInstrument` A、B 和 C 注册到 `PassContext` 并且 A 完成 `EnterPassContext()` 而 B 抛出异常,则永远不会执行 C;而执行 A 的 `ExitPassContext()`。 +* `InstrumentExitPassContext` + * 按照传递给 `PassContext` 的 `instruments` 的顺序执行每个 `PassInstrument` 实例的 `ExitPassContext()`。 + * 发生异常时,`instruments` 被清除。 + * 在抛出异常后注册的 `PassInstrument` 实例不执行 `ExitPassContext`。 +* `InstrumentBeforePass` + * 如果该 pass 未列为必需 pass,则执行 `ShouldRun`。 + * 如果 pass 没有被 `ShouldRun` 阻止,则 `RunBeforePass` 将按照 `instruments` 的顺序执行。 + * 请注意, `InstrumentBeforePass` 返回一个布尔值,指示是否应该运行 pass。 + * 当异常发生时,立即抛出。我们依赖 Python 上下文管理器安全退出 `PassContext`(意味着每个工具的 `ExitPassContext` 都会运行。对于 C++,参考 [include/tvm/support/with.h](https://github.com/apache/tvm/blob/main/include/tvm/support/with.h)。) +* `InstrumentAfterPass` + * `RunAfterPass` 按照传递给 `PassContext` 的 `instruments` 的顺序执行。 + * 当异常发生时,立即抛出。依靠 Python 上下文管理器或 `With` 类([include/tvm/support/with.h](https://github.com/apache/tvm/blob/main/include/tvm/support/with.h))安全退出 `PassContext`。 + +#### 内置工具 + +以下是几种内置工具。标有 *TODO* 的还没有实现。 + +* PassTimingInstrument(参考 [src/ir/instrument.cc](https://github.com/apache/tvm/blob/main/src/ir/instrument.cc)) + * 分析 pass 的执行时间 +* PrintIRBefore (TODO) + * 在 pass 转换之前打印 IR 模块。若在 pass 周围插入 `tvm.transform.PrintIR()` 也可以达到这个目的。但使用 `PassInstrument`,不需要修改 pass 的顺序。 +* PrintAfter (TODO) + * 在 pass 转换后打印 IR 模块。 + +### Python 前端 + +前端只需要一些简单的 API。例如,可以为用户提供以下 API 来创建和执行 pass(完整的实现在 [python/tvm/relay/transform/transform.py](https://github.com/apache/tvm/blob/main/python/tvm/relay/transform/transform.py) 和 [python/tvm/ir/transform.py](https://github.com/apache/tvm/blob/main/python/tvm/ir/transform.py) 中)。后端接收信息,并决定用哪个函数来创建 Pass 对象。 + +#### PassContext + +Python 前端为 `PassContext` 提供了一个 wrapper,通过覆盖 `__enter__` 和 `__exit__` 来启用 `with` 语法。提供一种 `current` 静态方法,供用户获取在一定范围内使用的上下文。 + +``` python +@tvm._ffi.register_object("transform.PassContext") +class PassContext(tvm.runtime.Object): + def __enter__(self): + _transform.EnterPassContext(self) + return self + + def __exit__(self, ptype, value, trace, config): + _transform.ExitPassContext(self) + + @staticmethod + def current(): + """返回当前 pass 上下文。""" + return _transform.GetCurrentPassContext() +``` + +`PassContext` 用于配置编译选项,包括优化级别和必需及禁用的 pass。它还接收一个配置字典,以便不同的 pass 可以方便地获取传递的数据,例如回退设备信息和循环展开的步长/深度等。为了能够获取所需的配置,必须通过 `TVM_REGISTER_PASS_CONFIG_OPTION` 注册密钥。例如,循环展开 pass 使用以下内容。 + +``` python +TVM_REGISTER_PASS_CONFIG_OPTION("tir.UnrollLoop", UnrollLoopConfig); +``` + +更多详情参阅 [src/tir/transforms/unroll_loop.cc](https://github.com/apache/tvm/blob/main/src/tir/transforms/unroll_loop.cc)。 + +#### Pass 对象 + +`Pass` 是所有 pass 对象的基类。这里的所有方法都只是在后端实现的简单 wrapper。是为方便用户与 Python 中的基类交互而定义的。在 pass 基类中只定义了一个 `__call__`,使子类成为可调用对象,进而可以轻松调用它们(例如 `pass_xx(arg)`)进行执行。 + +``` python +@register_relay_node +class Pass(RelayNode): + def __call__(self, mod): + return _transform.RunPass(self, mod) +``` + +提供一些辅助 API,方便从 Python 前端轻松创建 pass,并让 pass infra 控制执行。例如,`module_pass`、`function_pass` 和 `sequential` 提供给用户,方便用户自定义 pass 或 pass pipeline。 + +对于在 C++ 后端实现的所有 pass,分别在 [python/tvm/ir/transform.py](https://github.com/apache/tvm/blob/main/python/tvm/ir/transform.py) 和 [python/tvm/relay/transform/transform.py](https://github.com/apache/tvm/blob/main/python/tvm/relay/transform/transform.py) 中提供了相应的 Python API。例如,常量折叠有一个 Python API,如下所示: + +``` python +def FoldConstant(): + return _transform.FoldConstant() +``` + +通过装饰器进行装饰,创建一个 pass,如下所示: + +``` python + @relay.transform.module_pass(opt_level=2) + def transform(mod, ctx): + tp = relay.TensorType((10,), "float32") + x = relay.var("x", tp) + gv = relay.GlobalVar("abs") + func = relay.Function([x], relay.abs(x)) + new_mod = tvm.IRModule({gv: func}) + new_mod.update(mod) + return new_mod + +module_pass = transform +assert isinstance(module_pass, transform.ModulePass) +assert module_pass.info.opt_level == 2 +``` + +这里的 `transform` 函数为输入模块添加了一个 `abs` 函数,它可以是模块级别的任何自定义优化。创建此 `module_pass` 后,用户可以将其应用于任何 Relay 模块。例如,可以构建一个空模块,应用这个 pass,添加一个 `abs` 函数。 + +``` python +mod = tvm.IRModule() +mod = module_pass(mod) +``` + +相应地,也为 `function_pass` 提供了这样的功能。例如,函数级 pass 编写示例如下: + +``` python +@relay.transform.function_pass(opt_level=1) +class TestReplaceFunc: + def __init__(self, new_func): + self.new_func = new_func + def transform_function(self, func, mod, ctx): + # 仅用于演示 + # 将 func 转换为 new_func + return self.new_func + +x = relay.var("x", shape=(10, 20)) +f1 = relay.Function([x], x) +f2 = relay.Function([x], relay.log(x)) +# fpass 现在是一个特殊的 pass,它取代了每一个 +# 函数为 f1 +fpass = TestReplaceFunc(f1) +# 现在 input_mod 中的每个函数都被 f1 替换了 +res_mod = fpass(input_mod) +``` + +或者,用户也可以不使用装饰器直接注册一个 pass,然后调用它。有关如何自定义优化 pipeline 和调试 Relay 和 tir pass 的更多示例,参阅 [使用 pass 基础架构](https://github.com/apache/tvm/blob/main/tutorials/dev/use_pass_infra.py) 教程。 + +#### Pass Instrument + +可以通过在实现以下方法的类上使用 `pass_instrument` 装饰器([python/tvm/ir/instrument.py](https://github.com/apache/tvm/blob/main/python/tvm/ir/instrument.py))来实现 `PassInstrument`。注意,推荐使用 `pass_instrument` 装饰器来实现 `PassInstrument`,而不是覆盖或子类化。 + +* `enter_pass_ctx` + * 该方法在进入 `PassContext` 时运行。 +* `exit_pass_ctx` + * 此方法在退出 `PassContext` 时运行。 +* `should_run` + * 此方法在执行 pass 之前运行,返回一个布尔值,指示是否应该运行 pass。 +* `run_before_pass` + * 若要运行某 pass,则在 pass 执行之前运行此方法。 +* `run_after_pass` + * 此方法在执行 pass 后立即运行。 + * + +`PassInstrument` 实例可以通过 `tvm.transform.PassContext` 中的 `instruments` 参数注册。 + +[使用 pass Instrument](https://github.com/apache/tvm/blob/main/tutorials/dev/use_pass_instrument.py) 教程提供了如何使用 Python API 实现 `PassInstrument` 的示例。 + +#### 在当前 PassContext 中覆盖工具 + +提供 `override_instruments` 方法来覆盖当前 `PassContext` 的 `instruments`。例如,若在没有显式创建新 `PassContext` 的情况下运行 pass,仍然可以通过以下方式将 `PassInstrument` 注册到全局 `PassContext` 中: + +``` python +cur_pass_ctx = tvm.transform.PassContext.current() +# 覆盖 PassInstrument 实例 +cur_pass_ctx.override_instruments([pass_inst]) +mod = pass_seq(mod) +result = pass_inst.get_result() +``` + +注意,调用 `override_instruments` 同时也会调用旧 `PassInstrument` 实例的 `exit_pass_ctx` 方法。然后调用新 `PassInstrument` 的 `enter_pass_ctx` 方法。 diff --git a/versioned_docs/version-0.12.0/arch/arch/07-inferbound.md b/versioned_docs/version-0.12.0/arch/arch/07-inferbound.md new file mode 100644 index 00000000..f81765ae --- /dev/null +++ b/versioned_docs/version-0.12.0/arch/arch/07-inferbound.md @@ -0,0 +1,679 @@ +--- +title: InferBound Pass +sidebar_position: 170 +--- + +# InferBound Pass + +InferBound pass 在 normalize 之后、ScheduleOps [build_module.py](https://github.com/apache/tvm/blob/main/python/tvm/driver/build_module.py) 之前运行。InferBound 的主要工作是创建 bounds map,为程序中的每个 IterVar 指定一个 Range。接下来这些 bounds 会传递给 ScheduleOps,用于设置 For 循环的范围,参阅 [MakeLoopNest](https://github.com/apache/tvm/blob/main/src/te/operation/op_util.cc),以及设置分配缓冲区的大小([BuildRealize](https://github.com/apache/tvm/blob/main/src/te/operation/compute_op.cc))以及其他用途。 + +InferBound 的输出是从 IterVar 到 Range 的映射: + +``` c++ +Map InferBound(const Schedule& sch); +``` + +回顾 Range 和 IterVar 类: + +``` c++ +namespace HalideIR { +namespace IR { + class RangeNode : public Node { + public: + Expr min; + Expr extent; + // 剩余部分省略 + }; + }} + +namespace tvm { + class IterVarNode : public Node { + public: + Range dom; + Var var; + // 剩余部分省略 + }; +} +``` + +注意,IterVarNode 还包含一个 Range `dom`。这个 `dom` 的值是否有意义,取决于 IterVar 的创建时间。例如,调用 `tvm.compute` 时,会为每个 axis 和 reduce axis [创建一个 IterVar](https://github.com/apache/tvm/blob/main/src/te/operation/compute_op.cc) ,其中 dom 等于调用 `tvm.compute` 时提供的 shape。 + +另一方面,调用 `tvm.split` 时,会为内轴和外轴 [创建 IterVars](https://github.com/apache/tvm/blob/main/src/te/schedule/schedule_lang.cc),但这些 IterVars 没有被赋予有意义的 `dom` 值。 + +在任何情况下,IterVar 的 `dom` 成员在 InferBound 期间都不会被修改。但 IterVar 的 `dom` 成员有时用作 Range InferBound 计算的默认值。 + +为了理解 InferBound pass,我们先来看一下 TVM 代码库概念。 + +InferBound 接收一个参数,即 Schedule。这个 schedule 对象及其成员包含正在编译的程序的所有信息。 + +TVM schedule 由 stage 组成。每个 stage 只有一个 Operation,例如 ComputeOp 或 TensorComputeOp。每个 Operation 都有一个 root_iter_vars 列表,在 ComputeOp 的情况下,它由 axis IterVar 和 reduce axis IterVar 组成。 + +每个 Operation 还包含许多其他 IterVar,它们通过 Operation 的 IterVarRelations 列表相关联。每个 IterVarRelation 代表 schedule 中的 split、fuse 或 rebase。例如,在 split 的情况下,IterVarRelation 指定被拆分的父级 IterVar,以及两个子级 IterVar:内部和外部。 + +``` c++ +namespace tvm { + class ScheduleNode : public Node { + public: + Array outputs; + Array stages; + Map stage_map; + // 剩余部分省略 + }; + + class StageNode : public Node { + public: + Operation op; + Operation origin_op; + Array all_iter_vars; + Array leaf_iter_vars; + Array relations; + // 剩余部分省略 + }; + + class OperationNode : public Node { + public: + virtual Array root_iter_vars(); + virtual Array InputTensors(); + // 剩余部分省略 + }; + + class ComputeOpNode : public OperationNode { + public: + Array axis; + Array reduce_axis; + Array body; + Array root_iter_vars(); + // 剩余部分省略 + }; +} +``` + +在 TVM 的 context 中,张量表示操作的输出。 + +``` c++ +class TensorNode : public Node { +public: + // 源操作,可以是 None + // 这个 Tensor 是这个 op 输出的 + Operation op; + // 源操作的输出索引 + int value_index; +}; +``` + +上面的 Operation 类声明中,可以看到每个 operation 还有一个 InputTensor 列表。因此,schedule 的各个 stage 形成了一个 DAG,其中每个 stage 都是图中的一个节点。若 Stage B 的 operation 有一个输入张量,其源操作是 Stage A 的 op,那么图中从 Stage A 到 Stage B 有一个 edge。简而言之,若 B 消耗了一个由 A 产生的张量,则从 A 到 B 会出现一个 edge。参见下图。这个计算图是在 InferBound 开始时调用 [CreateReadGraph](https://github.com/apache/tvm/blob/main/src/te/schedule/bound.cc) 创建的。 + +![图片](/img/docs/tvmai/tvmai.github.io/main/images/docs/inferbound/stage_graph.png) + +InferBound 使 pass 遍历计算图,每个 stage 访问一次。InferBound 从输出 stage 开始(即上图中的实心蓝色节点),然后向上移动(在边缘的相反方向上)。这是通过对计算图的节点执行反向拓扑排序来实现的。因此,当 InferBound 访问一个 stage 时,它的每个 consumer stage 都已经被访问过。 + +![图片](/img/docs/tvmai/tvmai.github.io/main/images/docs/inferbound/inferbound_traversal.png) + +InferBound pass 如以下伪代码所示: + +``` c++ +Map InferBound(const Schedule& sch) { + Array outputs = sch->get_outputs(); + G = CreateGraph(outputs); + stage_list = sch->reverse_topological_sort(G); + Map rmap; + for (Stage s in stage_list) { + InferRootBound(s, &rmap); + PassDownDomain(s, &rmap); + } + return rmap; +} +``` + +InferBound pass 有两个不是很明显的属性: + +1. InferBound 访问一个 stage 后,stage 中所有 IterVar 的范围都会在 `rmap` 中设置。 +2. 每个 IterVar 的 Range 只在 `rmap` 中设置一次后就不会再变了。 + +因此,仍然需要解释 InferBound 在访问 stage 时的主要作用。从上面的伪代码中可以看出,InferBound 在每个 stage 调用了两个函数:InferRootBound 和 PassDownDomain。InferRootBound 的目的是设置 stage 每个 root_iter_var 的 Range(在 `rmap` 中)。(注意:InferRootBound 不设置任何其他 IterVar 的 Range,只设置属于 root_iter_vars 的那些)。PassDownDomain 的目的是将此信息传播到 stage 的其余 IterVars。当 PassDownDomain 返回时,stage 的所有 IterVars 在 `rmap` 中都有已知的 Range。 + +文档的其余部分将深入探讨 InferRootBound 和 PassDownDomain 的详细信息。由于 PassDownDomain 描述起来更简单,因此首先介绍它。 + +## IterVar Hyper-graph + +如上所述,InferBound pass 遍历 stage 计算图。但是,在每个 stage 中都有另一个节点为 IterVars 的计算图。 InferRootBound 和 PassDownDomain 在这些 IterVar 计算图上传递消息。 + +回想一下,stage 的所有 IterVar 都由 IterVarRelations 关联。一个 stage 的 IterVarRelations 构成一个有向无环 hyper-graph,计算图中每个节点对应一个 IterVar,每条 hyper-edge 对应一个 IterVarRelation。也可以将这个 hyper-graph 表示为 DAG,如下图所示更易于可视化。 + +![图片](/img/docs/tvmai/tvmai.github.io/main/images/docs/inferbound/relations.png) + +上图显示了一个 stage 的 IterVar hyper-graph。该 stage 有一个 root_iter_var `i`,它已被拆分,生成的内轴 `i.inner` 已再次拆分。该 stage 的 leaf_iter_vars 为绿色图示:`i.outer`、`i.inner.outer` 和 `i.inner.inner`。 + +消息传递函数被命名为「PassUp」或「PassDown」,取决于消息是从 DAG 中的子代传递给其父代(「PassUp」),还是从父代传递给其子代(「PassDown」)。例如,上图左侧的大箭头显示 PassDownDomain 从根 IterVar `i` 向其子 `i.outer` 和 `i.inner` 发送消息。 + +## PassDownDomain + +PassDownDomain 的作用是为 root_iter_vars 取 InferRootBound 产生的 Range,并设置 stage 中所有其他 IterVars 的 Range。 + +PassDownDomain 遍历 stage 的 IterVarRelations。IterVarRelation 有三种可能的类型:split、fuse 和 rebase。最有趣的案例(因为它还有改进空间)是表示 split 的 IterVarRelations。 + +根据父级 IterVar 的已知 Range,来设置 split 的内部 IterVar 和外部 IterVar 的 Range,如下: + +``` c++ +rmap[split->inner] = Range::FromMinExtent(0, split->factor) +rmap[split->outer] = Range::FromMinExtent(0, DivCeil(rmap[split->parent]->extent, split->factor)) +``` + +当 `split->factor` 没有平均划分父节点的范围时,就有机会收紧 InferBound 产生的边界。假设 parent 的范围是 20,split 因子是 16。那么在外部循环的第二次迭代中,内部循环只需要进行 4 次迭代,而非 16 次。如果 PassDownDomain 可以设置 `split->inner` 的范围为 `min (split->factor, rmap[split->parent]->extent - (split->outer * split->factor))`,则内部变量的范围将根据正在执行的外部循环的迭代进行适当调整。 + +对于 Fuse 关系,根据已知的内外 IterVar 的 Range 设置 fuse 后的 IterVar 的 Range,如下: + +``` c++ +rmap[fuse->fused] = Range::FromMinExtent(0, rmap[fuse->outer]->extent * rmap[fuse->inner]->extent) +``` + +## InferRootBound + +InferBound 调用 InferRootBound,然后在 stage 计算图中的每个 stage 调用 [PassDownDomain](#passdowndomain)。InferRootBound 的目的是设置 Stage 操作的每个 root_iter_var 的 Range。这些 Range 会用 [PassDownDomain](#passdowndomain) 传到 Stage 的其余 IterVars。注意,InferRootBound 不会设置任何其他 IterVar 的 Range,仅设置属于 Stage 的 root_iter_vars 的那些。 + +若 Stage 是输出 Stage 或占位符,InferRootBound 只需将 root_iter_var Range 设置为其默认值。root_iter_var 的默认 Range 取自 IterVar 的 `dom` 成员(参阅上面的 IterVarNode 类声明)。 + +否则,InferRootBound 将遍历 stage 的 consumer。为每个 consumer 的 IterVar 创建 IntSet,如下所示。 + +阶段 1)IntSet 为 consumer 的 leaf_iter_vars 初始化,并通过 PassUpDomain 传到 consumer 的 root_iter_vars(阶段 2)。这些 IntSet 用于创建 consumer stage(阶段 3)的输入张量的 TensorDom。最后,一旦所有 consumer 都处理完毕,InferRootBound 调用 GatherBound,根据 TensorDoms(阶段 4)设置 stage 的 root_iter_vars 的 Range。 + +这个过程看起来很复杂。原因之一是一个 stage 可以有多个 consumer。每个 consumer 都有不同的要求,且必须以某种方式整合。类似地,该 stage 可能会输出多个张量,并且每个 consumer 只使用这些张量的特定子集。此外,即使 consumer 使用特定的张量,它也可能不会使用张量的所有元素。 + +如上所述,consumer 可能只需要每个张量中的少量元素。consumer 可以看成是针对输出张量某些区域,向 stage 发出的请求。阶段 1-3 的工作是建立每个 consumer 所需的每个输出张量的区域。 + +![图片](/img/docs/tvmai/tvmai.github.io/main/images/docs/inferbound/inferbound_phases.png) + +### IntSet + +在 InferRootBound 期间,Range 被转换为 IntSet,并且在 IntSet 上执行消息传递。因此,了解 Range 和 IntSet 之间的区别很重要。「IntSet」这个名称表明它可以表示任意整数集,例如 A = {-10, 0, 10, 12, 13}。这肯定比 Range 更具表现力,Range 只表示一组连续的整数,例如 B = {10,11,12}。 + +然而,目前 IntSet 只有三种类型:IntervalSets、StrideSets 和 ModularSets。与 Range 类似,IntervalSets 仅表示连续整数的集合。StrideSet 由基本 IntervalSet、步长列表和范围列表定义。StrideSet 未被使用,ModularSet 只用于前端。 + +因此,目前在 TVM 中并非所有的整数集合都可以用 IntSet 来表示。例如,上例中的集合 A 不能用 IntSet 表示。将来 IntSet 的功能可以扩展为处理更通用的整数集,而无需对 IntSet 的用户进行修改。 + +*对于包含 compute_at 的 schedules**而言**,InferBound 更为复杂。因此首先**针对**不包含 compute_at 的 schedules**解读**InferBound。* + +### 阶段 1:为 consumer 的 leaf_iter_vars 初始化 IntSet + +``` c++ +/* + * 输入: Map rmap: 包含 consumer stage 的每个 IterVar 的 Range + * 输出: Map up_state: 包含 consumer 的每个 leaf_iter_var 的 IntSet + */ +``` + +在阶段 1,根据 `rmap` 中 leaf_iter_vars 的 Range 创建每个 consumer 的 leaf_iter_vars 的 IntSet。consumer 已经被 InferBound 访问过,所以它所有的 IterVar 都知道 `rmap` 中的 Range。 + +有以下三种案例: + +* 案例 1:leaf var 的 Range 范围为 1。这种情况下,leaf 的 up_state 只是一个点,等于 Range 的最小值。 +* 案例 2:*不需要释放。这种情况下,leaf 的 up_state 只是一个点,由 leaf var 本身定义。* +* 案例 3:需要释放。这种情况下,leaf 的 Range 被简单地转换为 IntSet。 + +简单起见,假设 schedule 不包含线程轴。这种情况下,仅当 schedule 包含 compute_at 时,才和案例 2 相关。参阅 [InferBound 与 compute_at](#inferboundca) 节来进一步获取更多信息。 + +### 阶段 2:将 IntSet 从 consumer 的 leaf 传到 consumer 的 root + +```c++ +/* + * Input: Map up_state: consumer leaf -> IntSet + * Output: Map dom_map: consumer root -> IntSet + */ +``` + +阶段 2 的目的是将 IntSet 信息从 consumer 的 leaf_iter_vars 传到 consumer 的 root_iter_vars。阶段 2 的结果是另一个映射 `dom_map`,其中包含每个 consumer 的 root_iter_vars 的 IntSet。 + +阶段 2 首先调用 PassUpDomain,它访问 consumer stage 的 IterVarRelations。在 Split 关系的情况下,PassUpDomain 根据内部和外部 IntSet 设置父级 IterVar 的 up_state,如下所示: + +* 案例 1:外部和内部 IterVar 的范围匹配它们的 `up_state` 域。在这种情况下,只需将父级的 Range 转换为 IntSet 即可设置父级的 `up_state`。 +* 案例 2:*否则,父级的* `up_state` *是相对于外部和内部的**`up_state`*通过评估* `outer*f + inner + rmap[parent]->min` *来定义的。这里,TVM 没有使用*s**plit 关系的因子,而是用* `f = rmap[inner]->extent`。 + +仅当 schedule 包含 compute_at 时才需要案例 2。参阅下面的 [InferBound 与 compute_at](#inferboundca) 节,进一步了解。 + +在 PassUpDomain 完成向 consumer 的所有 IterVars 传到 up_state 后,将创建一个从 root_iter_vars 到 IntSet 的新映射。如果 schedule 不包含 compute_at,则 root_iter_var iv 的 IntSet 由以下代码创建: + +``` c++ +dom_map[iv->var.get()] = IntSet::range(up_state.at(iv).cover_range(iv->dom)); +``` + +注意,若 schedule 不包含 compute_at,则实际上不需要阶段 1-2。dom_map 可以直接从 rmap 中的已知 Range 构建。Range 只需要转换为 IntSet,不会丢失信息。 + +### 阶段 3:将 IntSet 传到 consumer 的输入张量 + +``` c++ +/* + * Input: Map dom_map: consumer root -> IntSet + * Output: Map tmap: output tensor -> vector > + */ +``` + +注意,consumer 的输入张量是 InferBound 正在处理的 stage 的输出张量。因此,通过建立有关 consumer 输入张量的信息,实际上也获得了有关 stage 输出张量的信息:consumer 需要计算这些张量的某些区域。然后可以将该信息传到 stage 的其余部分,最终在阶段 4 结束时获得 stage 的 root_iter_vars 的 Range。 + +阶段 3 的输出是 tmap,它是一个包含所有 stage 输出张量的映射。张量是多维的,具有许多不同的轴。对于每个输出张量,以及每个张量的轴,tmap 包含一个 IntSet 列表。列表中的每个 IntSet 都是来自不同 consumer 的请求。 + +阶段 3 是通过在 consumer 上调用 PropBoundToInputs 来完成的。PropBoundToInputs 将 IntSet 添加到 tmap 的列表中,用于 consumer 的所有输入张量。 + +PropBoundToInputs 的具体行为取决于 consumer 操作的类型:ComputeOp、TensorComputeOp、PlaceholderOp、ExternOp 等。TensorComputeOp 的每个张量输入都有一个区域,定义了操作所依赖的张量切片。对于每个输入张量 i 和维度 j,根据 Region 中的相应维度向 tmap 添加一个请求: + +``` c++ +for (size_t j = 0; j < t.ndim(); ++j) { + // i selects the Tensor t + tmap[i][j].push_back(EvalSet(region[j], dom_map)); +} +``` + +### 阶段 4:整合所有 consumer + +```c++ +/* + * Input: Map tmap: output tensor -> vector > + * Output: Map rmap: rmap is populated for all of the stage's root_iter_vars + */ +``` + +阶段 4 由 GatherBound 执行,其行为取决于 stage 的操作类型。此处只讨论 ComputeOp,TensorComputeOp 情况类似。 + +ComputeOp 只有一个输出张量,其轴与 ComputeOp 的轴变量一一对应。ComputeOp 的 root_iter_vars 包括这些轴变量,以及 reduce_axis 变量。若 root IterVar 是一个轴变量,它对应一个输出张量的轴。 GatherBound 将此类 root IterVar 的 Range 设置为张量相应轴的所有 IntSet 的并集(即所有 consumer 请求的并集)。如果 root IterVar 是一个 reduce_axis,它的 Range 只是设置为其默认值(即 IterVarNode 的 `dom` 成员)。 + +``` c++ +// 'output' 选择输出张量 +// i 是维度 +rmap[axis[i]] = arith::Union(tmap[output][i]).cover_range(axis[i]->dom); +``` + +![图片](/img/docs/tvmai/tvmai.github.io/main/images/docs/inferbound/gatherbound.png) + +IntSet 的并集是通过将每个 IntSet 转换为一个区间来计算的,然后取所有最小值中的最小值,以及所有这些区间最大值中的最大值。 + +![图片](/img/docs/tvmai/tvmai.github.io/main/images/docs/inferbound/union.png) + +计算从未使用过的张量元素,显然会导致一些不必要的计算。 + +即使 IntervalSet 联合体不会产生非必要的计算,GatherBound 单独考虑张量的每个维度也会导致不必要的计算。例如,在下图中,两个 consumer A 和 B 需要 2D 张量的不相交区域:consumer A 需要 T[0:2, 0:2],consumer B 需要 T[2:4, 2:4]。 GatherBound 分别对张量的每个维度进行操作。对于张量的第一维,GatherBound 采用区间 0:2 和 2:4 的并集,产生 0:4(注意,此处不需要近似值)。对于张量的第二维也是如此。因此,这两个请求的维度并集为 T[0:4, 0:4]。因此 GatherBound 将导致计算张量 T 的所有 16 个元素,即使这些元素中只有一半会被使用。 + +![图片](/img/docs/tvmai/tvmai.github.io/main/images/docs/inferbound/gatherbound_problem.png) + +## InferBound 与 compute_at + +若 schedule 包含 compute_at,则 InferRootBound 的阶段 1-2 会变得更加复杂。 + +### 动机 + +**例 1** + +考虑以下 TVM 程序片段: + +``` python +C = tvm.compute((5, 16), lambda i, j : tvm.const(5, "int32"), name='C') +D = tvm.compute((5, 16), lambda i, j : C[i, j]*2, name='D') +``` + +会产生以下结果(简化的 IR): + +``` c++ +for i 0, 5 + for j 0, 16 + C[i, j] = 5 +for i 0, 5 + for j 0, 16 + D[i, j] = C[i, j]*2 +``` + +可以看出,stage D 需要计算 C 的所有(5,16)元素。 + +**例 2** + +然而,假设 C 在 D 的轴 j 处计算: + +``` python +s = tvm.create_schedule(D.op) +s[C].compute_at(s[D], D.op.axis[1]) +``` + +那么一次只需要一个 C 元素: + +``` c++ +for i 0, 5 + for j 0, 16 + C[0] = 5 + D[i, j] = C[0]*2 +``` + +**例 3** + +类似地,如果在 D 的 i 轴计算 C,则一次只需要一个包含 C 的 16 个元素的向量: + +``` c++ +for i 0, 5 + for j 0, 16 + C[j] = 5 + for j 0, 16 + D[i, j] = C[j]*2 +``` + +基于上述示例,很明显,InferBound 应该为 stage C 给出不同的答案,具体取决于它在其 consumer D 中「附加」的位置。 + +### 附加路径 + +若 stage C 在 stage D 的 j 轴上计算,我们说 C *附加*到 stage D 的轴 j。这通过设置以下三个成员变量反映在 Stage 对象中: + +``` c++ +class StageNode : public Node { +public: + // 省略 + // 对于compute_at,attach_type = kScope + AttachType attach_type; + + // 对于 compute_at,这是轴 + // 传递给 compute_at,例如 D.op.axis[1] + IterVar attach_ivar; + + // 传递给 compute_at 的阶段,例如 D + Stage attach_stage; + + // 省略 +}; +``` + +再次考虑上面的例子。为了让 InferBound 确定必须计算 C 的多少元素,重要的是,要知道 C 的计算是发生在 D 的叶变量的范围内,还是在该范围之上。在例 1 中,C 的计算发生在 D 的所有叶变量的范围*之上*。在例 2,C 的计算发生在 D 的所有叶变量的范围*内*。在例 3,C 出现在D 的 i 维度的范围内,但在 D 的 j 维度的范围之上。 + +CreateAttachPath 负责确定哪些作用域包含 stage C。这些作用域按从最内层到最外层的顺序排列。因此,对于每个 stage,CreateAttachPath 都会生成一个「附加路径」,其中列出了包含该 stage 从最里面到最外面的范围,在例 1,C 的附加路径为空。在例 2,C 的附加路径包含 `{j,i}`。在例 3,C 的附加路径是 `{i}`。 + +以下示例阐明了附加路径的概念,适用于更复杂的情况。 + +**例 4** + +``` python +C = tvm.compute((5, 16), lambda i, j : tvm.const(5, "int32"), name='C') +D = tvm.compute((4, 5, 16), lambda di, dj, dk : C[dj, dk]*2, name='D') +s = tvm.create_schedule(D.op) +s[C].compute_at(s[D], D.op.axis[2]) +``` + +这是 ScheduleOps 之后的 IR(注意,使用 ScheduleOps 的 `debug_keep_trivial_loop` 参数保留了范围为 1 的循环): + +``` c++ +realize D([0, 4], [0, 5], [0, 16]) { + produce D { + for (di, 0, 4) { + for (dj, 0, 5) { + for (dk, 0, 16) { + realize C([dj, 1], [dk, 1]) { + produce C { + for (i, 0, 1) { + for (j, 0, 1) { + C((i + dj), (j + dk)) =5 + } + } + } + D(di, dj, dk) =(C(dj, dk)*2) + } + } + } + } + } +} +``` + +在这种情况下,C 的附加路径是 `{dk, dj, di}`。注意 C 没有使用 di,但 di 仍然出现在 C 的附加路径中。 + +**例 5** + +根据上述定义,可以很自然地在拆分后应用 Compute_at。下面例子中,C 的附着点是 D 的 j_inner。C 的附着路径是 `{j_inner, j_outer, i}`。 + +``` python +C = tvm.compute((5, 16), lambda i, j : tvm.const(5, "int32"), name='C') +D = tvm.compute((5, 16), lambda i, j : C[i, j]*2, name='D') +s = tvm.create_schedule(D.op) +d_o, d_i = s[D].split(D.op.axis[1], factor=8) +s[C].compute_at(s[D], d_i) +``` + +这个案例的 IR 如下所示: + +``` c++ +for i 0, 5 + for j_outer 0, 2 + for j_inner 0, 8 + C[0] = 5 + D[i, j_outer*8 + j_inner] = C[0]*2 +``` + +### 构建附加路径 + +继续参考上一节中介绍的 stage C 和 D。CreateAttachPath 算法按照如下方式构建 stage C 的附加路径。若 C 没有 attach_type `kScope`,则 C 没有附加内容,C 的附加路径为空;否则,在 attach_stage=D 处附加 C。 + +以自上而下的顺序遍历 D 的 leaf 变量。所有从 C.attach_ivar 或是更低位置开始的 leaf 变量都添加到 C 的附加路径中。然后,若 D 也附加到某个地方,例如 stage E ,则对 E 的 leaf 重复该过程。因此 CreateAttachPath 继续向 C 的附加路径添加变量,直到遇到没有附加的 stage。 + +在下面的示例中,C 附加到 D,D 附加到 E。 + +``` python +C = tvm.compute((5, 16), lambda ci, cj : tvm.const(5, "int32"), name='C') +D = tvm.compute((5, 16), lambda di, dj : C[di, dj]*2, name='D') +E = tvm.compute((5, 16), lambda ei, ej : D[ei, ej]*4, name='E') +s = tvm.create_schedule(E.op) +s[C].compute_at(s[D], D.op.axis[1]) +s[D].compute_at(s[E], E.op.axis[1]) +``` + +当 `debug_keep_trivial_loop=True` 时,C 的附加路径为 `{dj,di,ej,ei}`,D 的附加路径为 `{ej,ei}`: + +``` c++ +// attr [D] storage_scope = "global" +allocate D[int32 * 1] +// attr [C] storage_scope = "global" +allocate C[int32 * 1] +produce E { + for (ei, 0, 5) { + for (ej, 0, 16) { + produce D { + for (di, 0, 1) { + for (dj, 0, 1) { + produce C { + for (ci, 0, 1) { + for (cj, 0, 1) { + C[(ci + cj)] = 5 + } + } + } + D[(di + dj)] = (C[(di + dj)]*2) + } + } + } + E[((ei*16) + ej)] = (D[0]*4) + } + } +} +``` + +### InferBound 与 compute_at + +前面已经介绍了附加路径的概念,现在来看,若 schedule 包含 compute_at 时,InferBound 的不同之处。唯一的区别在于 InferRootBound,[阶段 1:为 consumer 的 leaf_iter_vars 初始化 IntSet ](#phase1)和 [阶段 2:将 IntSet 从 consumer 的 leaf 传到 consumer 的 root](#phase2)。 + +在 InferRootBound 中,目标是确定特定 stage C 的 root_iter_vars 的 Range。InferRootBound 的阶段 1-2 将 IntSet 分配给 C consumer 的 leaf IterVar,然后将这些 IntSet 传到 consumer 的 root_iter_vars。 + +若没有附加,则已经为 consumer 变量计算的 Range 定义了 consumer 需要多少 C。但是,若 stage 实际上在 consumer 变量 j 的一个范围内,那么一次只需要 j 的范围内的一个点。 + +### 阶段 1:为 consumer 的 leaf_iter_vars 初始化 IntSet + +``` c++ +/* + * 输入:Map rmap: contains the Range for each IterVar of the consumer stage + * 输出:Map up_state: contains an IntSet for each leaf_iter_var of the consumer + */ +``` + +阶段 1,根据 rmap 中的 leaf_iter_vars 的 Range 创建每个 consumer 的 leaf_iter_vars 的 IntSet。consumer 已经被 InferBound 访问过,所以它的所有 IterVar 都知道 rmap 中的 Range。 + +有以下三种案例: + +* 案例 1:leaf var 的 Range 范围为 1。这种情况下,leaf 的 up_state 只是一个点,等于 Range 的最小值。 +* 案例 2:不需要释放。这种情况下,leaf 的 up_state 只是一个点,由 leaf var 本身定义。 +* 案例 3:需要释放。这种情况下,leaf 的 Range 被简单地转换为 IntSet。 + 若在 consumer 中遇到 stage C 的附着点,就会发生案例 2。对于此 attach_ivar,以及 consumer 的所有更高叶变量,将应用案例 2。若 C 在叶变量的 Range 内,这将确保仅请求叶变量范围内的单个点。 + +### 阶段 2:将 IntSet 从 consumer 的 leaf 传到 consumer 的 root + +``` c++ +/* + * Input: Map up_state: consumer leaf -> IntSet + * Output: Map dom_map: consumer root -> IntSet + */ +``` + +阶段 2 首先调用 PassUpDomain,它访问 consumer stage 的 IterVarRelations。在 Split 关系的情况下,PassUpDomain 根据内部和外部 IntSet 设置父级 IterVar 的 up_state,如下所示: + +* 案例 1:外部和内部 IterVar 的 Range 匹配它们的 `up_state` 域。在这种情况下,只需将父级的 Range 转换为 IntSet 即可设置父级的 `up_state`。 +* 案例 2:否则,父级的 `up_state` 是通过评估 `outer*f + inner + rmap[parent]->min` 来定义的,相对于外部和内部的 `up_state`。在这里,TVM 没有使用 split 关系的因子,而是使用* `f = rmap[inner]->extent`。 + +由于 schedule 包含 compute_at,因此可以应用案例 2。这是因为 leaf IntSet 现在可能会被初始化为其 Range 内的单个点([阶段 1 的案例 2:为 consumer 的 leaf_iter_vars 初始化 IntSet](#phase1ca)),因此 IntSet 无法总是与 Range 匹配。 + +PassUpDomain 将 up_state 向 consumer 传给所有 IterVars 后,将创建一个从 root_iter_vars 到 IntSet 的新映射。若 stage 没有附加到当前 consumer,那么对于 consumer 的 attach_path 中的每个变量 iv,将 iv 的 Range 添加到一个 `relax_set`。stage 的 root 变量是根据这个 `relax_set` 进行评估的。 + +这是为了处理类似以下示例的情况,其中 C 没有附加到任何地方,但它的 consumer D 在 stage E 中附加。这种情况下,在确定 C 有多少需要计算时,必须考虑 D 的 attach_path,`{ej,ei}`。 + +``` python +C = tvm.compute((5, 16), lambda ci, cj : tvm.const(5, "int32"), name='C') +D = tvm.compute((5, 16), lambda di, dj : C[di, dj]*2, name='D') +E = tvm.compute((5, 16), lambda ei, ej : D[ei, ej]*4, name='E') +s = tvm.create_schedule(E.op) +s[D].compute_at(s[E], E.op.axis[1]) +``` + +``` c++ +for ci 0, 5 + for cj 0, 16 + C[ci, cj] = 5 +for ei 0, 5 + for ej 0, 16 + D[0] = C[ei, ej]*2 + E[ei, ej] = D[0]*4 +``` + +### PassUpDomain 的限制 + +本节介绍 PassUpDomain 的已知限制。这些限制会影响 InferBound 生成的 Range,以及 PassUpDomain 的其他用户(例如 `tensorize`)。 + +**例 6** + +上面仅讨论了 PassUpDomain 在 Split 关系上的行为。在以下示例中,schedule 除了 `split` 之外还包含 `fuse`。以下 TVM 程序中,operation C 有两个轴被融合,然后融合的轴被拆分。注意,所有张量最初的 shape 都是 `(4, 4)`,并且融合轴也被因子 `4` 分割。假设 fuse 的效果只是被 split 所抵消。然而,在 TVM 中并非如此,如下所述。 + +``` python +import tvm +from tvm import te + +n = 4 +m = 4 + +A = te.placeholder((n, m), name='A') +B = te.compute((n, m), lambda bi, bj: A[bi, bj]+2, name='B') +C = te.compute((n, m), lambda ci, cj: B[ci, cj]*3, name='C') + +s = te.create_schedule(C.op) + +fused_axes = s[C].fuse(C.op.axis[0], C.op.axis[1]) +xo, xi = s[C].split(fused_axes, 4) + +s[B].compute_at(s[C], xo) + +print(tvm.lower(s, [A, C], simple_mode=True)) +``` + +该程序的输出如下所示。注意,每次通过外循环计算 B 的所有 16 个元素,即使 C 只使用其中的 4 个。 + +``` c++ +// attr [B] storage_scope = "global" +allocate B[float32 * 16] +produce C { + for (ci.cj.fused.outer, 0, 4) { + produce B { + for (bi, 0, 4) { + for (bj, 0, 4) { + B[((bi*4) + bj)] = (A[((bi*4) + bj)] + 2.000000f) + } + } + } + for (ci.cj.fused.inner, 0, 4) { + C[((ci.cj.fused.outer*4) + ci.cj.fused.inner)] = (B[((ci.cj.fused.outer*4) + ci.cj.fused.inner)]*3.000000f) + } + } +} +``` + +这与下面的 IR 形成对比,后者是通过删除 fuse 和 split 修改上述程序,并将 compute_at 替换为 `s[B].compute_at(s[C], C.op.axis[0])` ,注意,在下面的 IR 中,根据需要一次只计算 B 的 4 个元素。缓冲区 B 也更小。 + +``` c++ +// attr [B] storage_scope = "global" +allocate B[float32 * 4] +produce C { + for (ci, 0, 4) { + produce B { + for (bj, 0, 4) { + B[bj] = (A[((ci*4) + bj)] + 2.000000f) + } + } + for (cj, 0, 4) { + C[((ci*4) + cj)] = (B[cj]*3.000000f) + } + } +} +``` + +这个例子表明,与预期相反,split 并非只是抵消 fuse。那么造成这种差异的原因是什么?当一次实际上只需要一行时,为什么要重新计算整个张量 B 4 次? + +InferBound 的任务是确定必须计算的 B 的数量。但是,在这种情况下,InferBound 为 B 的 root_iter_vars 返回的范围太大:对于 `bi` 和 `bj` 都是 `[0, 4]`。这是因为 PassUpDomain 对 Fuse 关系的限制,后续将进行详细解释。 + +当 InferRootBound 在 stage B 工作时,它会访问 B 的 consumer stage C,以了解 C 请求了多少 B。C 有 root_iter_vars ci 和 cj,已经融合并进行了分割。这导致了 stage C 的以下 [IterVar Hyper-graph](https://tvm.apache.org/docs/arch/inferbound.html#itervarhypergraph)。 + +![图片](/img/docs/tvmai/tvmai.github.io/main/images/docs/inferbound/passupdomain_problem.png) + +在 stage B 上跟踪 InferRootBound 的执行。[阶段 1:为 InferRootBound 的 consumer leaf_iter_vars 初始化 IntSet](#phase1ca) 涉及为 B 的 consumer stage C 的所有 leaf_iter_vars 设置 IntSet。在这种情况下,C 的 leaf_iter_vars 是 `ci.cj.fused.outer` 和 `ci.cj.fused.inner`。由于 B 附加在 `ci.cj.fused.outer` 处,因此 `ci.cj.fused.inner` 必须释放,但 `ci.cj.fused.outer` 是单点。 C 的 leaf_iter_vars 的 IntSet,在 [阶段 1:为 consumer leaf_iter_vars 初始化 IntSet](#phase1ca) 之后,如下表所示。 + +| **IterVar** | **IntSet****after Phase 1** | +|:---|:---| +| ci.cj.fused.inner | [0, (min(4, (16 - (ci.cj.fused.outer*4))) - 1)] | +| ci.cj.fused.outer | [ci.cj.fused.outer, ci.cj.fused.outer] | + +在 InferRootBound 的 [阶段 2:将 IntSet 从 consumer leaf 传到 consumer root](#phase2ca) 中,以自下而上的顺序在所有 C 的 IterVarRelations 上调用 PassUpDomain。 + +PassUpDomain 首先在 C 的 Split 节点上调用。PassUpDomain 的案例 2 适用,因为 `ci.cj.fused.outer` 的 IntSet 只是一个点,并且不等于它的 Range(如先前在 stage C 上由 InferBound 计算的那样)。因此,PassUpDomain 根据 `ci.cj.fused.inner` 和 `ci.cj.fused.outer` 的 IntSet 设置 `ci.cj.fused` 的 IntSet,如下表第 3 行所示。 + +| **IterVar** | **IntSet****after PassUpDomain on SplitNode** | +|:---|:---| +| ci.cj.fused.inner | [0, (min(4, (16 - (ci.cj.fused.outer*4))) - 1)] | +| ci.cj.fused.outer | [ci.cj.fused.outer, ci.cj.fused.outer] | +| ci.cj.fused | [(ci.cj.fused.outer*4), ((ci.cj.fused.outer*4) + (min(4, (16 - (ci.cj.fused.outer*4))) - 1))] | + +在 Split 节点调用 PassUpDomain 后,在 Fuse 节点也进行调用。 + +* 案例 1:IterVar`fused` 的 Range(如先前由 InferBound 计算的那样)等于其 IntSet +* 案例2:IterVar `fused` 的 IntSet 是单点 +* 案例3:其他情况 + +示例中,`ci.cj.fused` 的 Range 是 [0, 16)。不同于 `ci.cj.fused` 的 IntSet,其范围最多为 4(见上表第 3 行)。因此案例 1 不适用。案例 2 也不适用,因为 `ci.cj.fused` 的 IntSet 不是单点。因此,仅适用于默认案例 3。 + +在案例 3 中,PassUpDomain 保守地应用了「回退(fallback)推理规则」,即它只返回等于 `ci` 和 `cj` 的 Range 的 IntSet。由于 C 是 schedule 的输出 stage,InferBound 会将 C 的 root_iter_vars(即 `ci` 和 `cj`)的 Range 设置为它们的原始维度(即它们的 IterVars 的 `dom` 值)。`ci` 和 `cj` 的 PassUpDomain 的结果输出显示在下表的最后两行中。 + +| **IterVar** | **IntSet****after PassUpDomain on FuseNode** | +|:---|:---| +| ci.cj.fused.inner | [0, (min(4, (16 - (ci.cj.fused.outer*4))) - 1)] | +| ci.cj.fused.outer | [ci.cj.fused.outer, ci.cj.fused.outer] | +| ci.cj.fused | [(ci.cj.fused.outer*4), ((ci.cj.fused.outer*4) + (min(4, (16 - (ci.cj.fused.outer*4))) - 1))] | +| ci | [0, 4] | +| cj | [0, 4] | + +这足以保证 consumer C 请求 B 的*所有*元素:`ci` 和 `cj` 的 IntSet 成为 consumer C 对 stage B 输出张量的请求(通过 [阶段 3:将 IntSet 传到 consumer 的输入张量](https://tvm.apache.org/docs/arch/inferbound.html#phase3) 中的 PropBoundToInputs 和 [阶段 4:整合所有 consumer](https://tvm.apache.org/docs/arch/inferbound.html#phase4) 中的 GatherBound)。 + +此示例表明,包含融合轴拆分的 schedule 很难在 TVM 中处理。难度源自 GatherBound 的限制。consumer C 请求的张量 B 的区域必须是 B 的单个矩形区域。或者,若 B 有两个以上的维度,则 B 区域必须可表示为每个轴的独立 Range。 + +若 split 因子为 4 或 8,以上示例中,外循环每次迭代所需的 B 区域是矩形的。 + +![图片](/img/docs/tvmai/tvmai.github.io/main/images/docs/inferbound/passupdomain_div.png) + +但是,若上例中的拆分因子从 4 变为 3,则很容易看出,C 所需要的 B 区域无法继续通过其每个轴的独立 Range 来描述了。 + +![图片](/img/docs/tvmai/tvmai.github.io/main/images/docs/inferbound/passupdomain_nodiv.png) + +下图显示了矩形区域所能达到的最佳效果。橙色区域是在外循环的每次迭代中覆盖需要计算的 B 区域的最小矩形区域。 + +![图片](/img/docs/tvmai/tvmai.github.io/main/images/docs/inferbound/passupdomain_min.png) diff --git a/versioned_docs/version-0.12.0/arch/arch/08-hybrid_script.md b/versioned_docs/version-0.12.0/arch/arch/08-hybrid_script.md new file mode 100644 index 00000000..8c54ea84 --- /dev/null +++ b/versioned_docs/version-0.12.0/arch/arch/08-hybrid_script.md @@ -0,0 +1,56 @@ +--- +title: 混合前端开发者指南 +sidebar_position: 180 +--- + +# 混合前端开发者指南 + +对于这样的开发者: + +1. 尝试编写一些 TVM 尚未支持的初步模式,[混合前端语言参考手册](https://tvm.apache.org/docs/reference/langref/hybrid_script.html#hybrid-langref-label) 可能很有用。 +2. 想要知道模块的实现细节,那么请详细阅读本节! + +## 特征 + +### 软件仿真 + +在软件仿真中,最有趣的是 `tvm.te.hybrid.script` 装饰器。它的作用: + +1. 导入 runtime 变量 +2. 根据传递的参数重载函数 + +如果我说的不对请纠正我:我认为上面 1. 的实现很危险,但别无选择。我只是将这些名称添加到 Python 字典 `func.__global__` 中,`func` 调用完后,将清除这些名称。 + +重载很简单:装饰器会检查参数的类型,并决定实际要调用哪个函数。 + +### 后端编译 + +编译是一个大模块,更多细节参阅 `python/tvm/te/hybrid/`。第一阶段确定用法,或者更准确地说,是每个变量的声明,第二阶段进行实际的 IR 生成。 + +### 属性 + +目前**仅**支持张量的 shape 属性。参考 `python/tvm/te/hybrid/parser.py` 中的 `visit_Subscript`,了解更多详细信息。这是一种侵入式的解决方案,只在下标处检查了属性。 + +### 循环 + +HalideIR 中的循环共有 4 种类型:`serial`,`unrolled`,`parallel` 和 `vectorized`。 + +:::note +与 HalideIR 不同,在 `loop_type(a, b)` 中,`a` 是起点,`b` 是迭代的行程计数。这里的 `loop_type(a, b)` 表示 `[a, b)`。因此,当将其降级到 HalideIR 时,要执行 `start, extent = a, b - a`。 +::: + +:::note +它们在 HalideIR 中是被动形式的枚举。这里因为已准备好运行它们,所以用主动形式来注解循环。 +::: + +### 变量 + +因为 `HalideIR` 中没有变量,因此所有可变的变量都会被降级为大小为 1 的数组。它将变量的第一次存储作为其声明。 + +### 数学内联函数 + +目前支持这些数学内联函数:`log`,`exp`,`sigmoid`,`tanh`,`power` 和 `popcount`。数学内联函数由装饰器导入。大多数内联函数都是借用库的实现,除了 `popcount` 和 `sigmoid` 是手动实现的。 + +### 转换 + +可以用关键字 `uint8`,`uint16`,`uint32`,`uint64`,`int8`,`int16`,`int32`,`int64`,`float16`,`float32` 和 `float64` 来转换值。 \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/arch/arch/09-relay_intro.md b/versioned_docs/version-0.12.0/arch/arch/09-relay_intro.md new file mode 100644 index 00000000..c4229cfb --- /dev/null +++ b/versioned_docs/version-0.12.0/arch/arch/09-relay_intro.md @@ -0,0 +1,133 @@ +--- +title: Relay IR 简介 +sidebar_position: 190 +--- + +# Relay IR 简介 + +本文介绍 Relay IR——第二代 NNVM。学习本节内容,需要具备一定的编程背景,以及熟悉计算图表示的深度学习框架。 + +学习本节内容,你将了解: + +* 支持传统的数据流式编程和转换。 +* 支持函数式作用域、let-binding,并使其成为功能齐全的可微分语言。 +* 允许用户混合两种编程风格。 + +## 使用 Relay 构建计算图 + +传统的深度学习框架使用计算图作为中间表示(IR)。计算图(或数据流图)是表示计算的有向无环图(DAG)。尽管数据流图由于缺乏控制流,而受限于能表达的计算,但其易用性极大简化了实现自动微分和编译异构执行环境(例如,在专用硬件上执行部分图)。 + +![图片](/img/docs/tvmai/tvmai.github.io/main/images/relay/dataflow.png) + +您可以使用 Relay 构建计算(数据流)图。具体来说,上面的代码展示了如何构造一个简单的两节点图。您会发现该示例的语法与现有的计算图 IR(如 NNVMv1)没有太大区别,唯一的区别在于术语: + +* 现有框架通常使用计算图和子图 +* Relay 使用函数,例如– `fn (%x)`,表示计算图 + +每个数据流节点(dataflow node)都是 Relay 中的一个 CallNode。Relay Python DSL 允许快速构建数据流图(dataflow graph)。以上代码强调显式地构造了一个 Add 节点,其两个输入点都指向 `%1`。深度学习框架评估上述程序时,会按照拓扑顺序计算节点,`%1` 只会计算一次。 + +尽管这种现象对于深度学习框架的构建者来说很常见,但对于 PL 研究人员来说可能并非如此。如果实现一个简单的 visitor 来打印结果,并将结果视为嵌套调用表达式,它会变成 `log(%x) + log(%x)`。 + +当 DAG 中存在共享节点时,这种歧义是由对程序语义的不同解释引起的。在正常的函数式编程 IR 中,嵌套表达式被视为表达式树,其没有考虑到 `%1` 实际上在 `%2` 中重复使用了两次这一事实。 + +Relay IR 关注了这种差异。通常,深度学习框架用户以这种方式构建计算图,经常发生 DAG 节点重用。因此,当以文本格式打印出 Relay 程序时,每行打印一个 CallNode,并为每个 CallNode 分配一个临时 id `(%1, %2)`,以便在程序的后面部分可以引用每个公共节点。 + +## 模块:支持多个函数(计算图) + +到目前为止,已经介绍了如何将数据流图构建为函数。有人自然会问:我们能不能支持多种功能,让它们互相调用?Relay 允许将多个函数组合在一个模块中;下面的代码展示了一个函数调用另一个函数的示例。 + +``` python +def @muladd(%x, %y, %z) { + %1 = mul(%x, %y) + %2 = add(%1, %z) + %2 +} +def @myfunc(%x) { + %1 = @muladd(%x, 1, 2) + %2 = @muladd(%1, 2, 3) + %2 +} +``` + +模块可以看作是一个 `Map`。这里 GlobalVar 只是一个 id,用于表示模块中的函数。 `@muladd` 和 `@myfunc` 在上面的例子中是 GlobalVars。 + +当一个 CallNode 用于调用另一个函数时,对应的 GlobalVar 存储在 CallNode 的 op 字段中。它包含一个间接级别——要使用相应的 GlobalVar 从模块中查找被调用函数的主体。 + +在这种特殊情况下,也可以直接将 Function 的引用作为 op 存储在 CallNode 中。那么,为什么要引入 GlobalVar 呢?主要原因是 GlobalVar 将定义/声明解耦,并启用函数的递归和延迟声明。 + +``` python +def @myfunc(%x) { + %1 = equal(%x, 1) + if (%1) { + %x + } else { + %2 = sub(%x, 1) + %3 = @myfunc(%2) + %4 = add(%3, %3) + %4 + } +} +``` + +以上示例 `@myfunc` 递归调用自身。使用 GlobalVar `@myfunc` 来表示函数,避免了数据结构中的循环依赖。至此,已经介绍了 Relay 中的基本概念。注意,Relay 比 NNVMv1 有以下改进: + +* 简洁的文本格式,简化了编写过程的调试。 +* 联合模块中对子图函数的最好的支持,使得联合优化的可能性更大,例如内联和调用约定规范。 +* 原生的前端语言互用性,例如,所有数据结构都可以在 Python 中访问,这使得用户可以在 Python 中快速构建优化原型,并将其与 C++ 代码混合。 + +## let 绑定和范围 + +到目前为止,已经介绍了如何用深度学习框架中旧的方式来构建计算图。本节将讨论 Relay 引入的一个新的重要结构——let 绑定。 + +所有高级编程语言都使用 let 绑定。在 Relay 中,它是一个具有三个字段 `Let(var, value, body)` 的数据结构。评估 let 表达式时,首先评估 value 部分,将其分配给 var,然后在 body 表达式中返回评估结果。 + +可以使用一系列 let 绑定,来构造与数据流程序逻辑等效的程序。以下代码示例并排显示了一个具有两个表单的程序。 + +![图片](/img/docs/tvmai/tvmai.github.io/main/images/relay/dataflow_vs_func.png) + +嵌套的 let 绑定称为 A 范式,通常用作函数式编程语言中的 IR。仔细看看 AST 结构。虽然这两个程序在语义上是相同的(它们的文本表示也是相同的,除了 A-normal 形式有 let 前缀),但 AST 结构是不同的。 + +由于程序优化采用这些 AST 数据结构,并对其进行转换,因此这两种不同的结构会影响将要编写的编译器代码。例如,若要检测一个模式 `add(log(x), y)`: + +* 在数据流形式中,可以先访问 add 节点,然后直接看它的第一个参数是不是日志。 +* 在 A 范式中,不能再直接进行检查,因为要添加的第一个输入是 `%v1`——保留一个从变量到其绑定值的映射,并查找该映射,以便知道 `% v1` 是一个日志。 + +不同的数据结构会影响编写转换的方式,请牢记这一点。所以,作为一个深度学习框架的开发者,你可能会问,为什么我们需要 let 绑定?你的 PL 朋友总是会告诉你 let 很重要——因为 PL 是一个相当成熟的领域,其背后一定有一些智慧。 + +## 为什么需要 let 绑定 + +let 绑定的一个关键用法是它指定了计算范围。下面的例子没有使用 let 绑定: + +![图片](/img/docs/tvmai/tvmai.github.io/main/images/relay/let_scope.png) + +当试图决定应该在哪里评估节点 `%1` 时,问题就来了。尤其是,尽管文本格式建议在 if 范围之外评估节点 `%1`,但 AST(如图所示)并不建议这样做。实际上,数据流图从未定义其评估范围。这在语义上引入了一些歧义。 + +当有闭包时,这种歧义变得更加有趣。下面的程序返回一个闭包。我们不确定在哪里计算 `%1`;它可以在闭包的内部,也可以在闭包外部。 + +``` python +fn (%x) { + %1 = log(%x) + %2 = fn(%y) { + add(%y, %1) + } + %2 +} +``` + +let 绑定解决了这个问题,因为值的计算发生在 let 节点。在这两个程序中,如果将 `%1 = log(%x)` 改为 `let %v1 = log(%x)`,就可以清楚地指定计算位置在 if 范围和闭包之外。可见 let-binding 给出的计算站点的规范更精确,并且在生成后端代码时很有用(跟 IR 中的规范类似)。 + +另一方面,不指定计算范围的数据流形式确实有其自身的优势——即我们在生成代码时无需担心将 let 放在哪里。数据流形式还为后面的传递提供了更多自由来决定将评估点放在哪里。因此,当您发现方便时,在优化的初始阶段使用程序的数据流形式可能不是一个坏主意。如今,Relay 中的许多优化都是为了优化数据流程序而编写的。 + +但是,当将 IR 降级到实际的 runtime 程序时,需要精确计算范围。特别是,使用子函数和闭包时,我们希望明确指定计算范围。在后期执行特定优化中 let-binding 可用于解决此问题。 + +## IR 转换的影响 + +希望你现在已经熟悉这两种表示形式。大多数函数式编程语言以 A 范式进行分析,其中分析器不需要注意表达式是否为 DAG。 + +Relay 同时支持数据流形式和 let 绑定。让框架开发者选择他们熟悉的表示很重要。然而,这确实对于编写 pass 有一些影响: + +* 如果你具备数据流相关知识,且要处理 let,将 var 映射到表达式,以便在遇到 var 时查找。因为任何情况下都需要一个从表达式到转换表达式的映射,所以这可能意味着最少的变更。注意,这将有效地删除程序中的所有 let。 +* 如果你具备 PL 相关知识,且熟悉 A 范式,我们将为 A 范式 pass 提供数据流。 +* 对于 PL 开发者而言,在实现某些东西(比如数据流到 ANF 的转换)时,表达式可以是 DAG,这通常意味着应该使用 `Map` 访问表达式,并且转换后的表达式结果只计算一次,因此结果表达式保持通用结构(common structure)。 + +本节内容未涵盖的高阶概念,如符号 shape 推断、多态函数,欢迎查看其他材料。 diff --git a/versioned_docs/version-0.12.0/arch/arch/10-relay_op_strategy.md b/versioned_docs/version-0.12.0/arch/arch/10-relay_op_strategy.md new file mode 100644 index 00000000..07174427 --- /dev/null +++ b/versioned_docs/version-0.12.0/arch/arch/10-relay_op_strategy.md @@ -0,0 +1,173 @@ +--- +title: Relay 算子策略 +sidebar_position: 200 +--- + +# Relay 算子策略 + +为了将 Relay 算子降级为 TOPI 库中定义的实现,需要为每个 Relay 算子注册一个 compute 和 schedule 函数。然而,compute 和 schedule 函数通常是针对每个 target 定制化的,而且,即使对于同一个 target,也可能有多种可用算法和实现。算子策略是为了应对这种复杂的局面而引入的,它让开发者可以为每个算子和 target 定义灵活的降级策略。 + +## 算子策略设计 + +算子策略的基本元素是 `OpImplementation`。它包括一对 compute 和 schedule 函数、实现的名称和优先级(优先级的使用在 [从算子策略中选择实现](#select-implementation-from-op-strategy) 中进行了解释)。 + +`OpStrategy` 包括一个 `OpSpecialization` 列表。每个 `OpSpecialization` 都包含一个与 `SpecializedCondition` 关联的 `OpImplementation` 列表(参见 `include/tvm/te/schedule.h` 中的定义)。 `SpecializedCondition` 可以为 null,表示实现普遍适用;否则,仅在满足特定条件时才考虑实现。`SpecializedCondition` 包含一个子句列表(它们在张量表达式中以联合范式(CNF)的形式定义),并且仅支持在张量 shape 上的情况。 + +最后,策略函数或 `FTVMStrategy` 确定在给定工作负载的情况下,使用哪一对 compute 和 schedule 函数,并且要注册到每个 Relay 算子。 `FTVMStrategy` 是一个通用函数(参见 `include/tvm/target/generic_func.h`),所有的 target 都可以覆盖它。函数签名是 + +``` c++ +OpStrategy(const Attrs& attrs, const Array& inputs, const Type& out_type, const Target& target) +``` + +该函数在给定 op 属性、输入张量、输出类型和要编译到的 target 的情况下,可返回一个 `OpStrategy`。 + +## 编写策略函数 + +推荐开发者用 Python 编写策略函数,因为大多数 TOPI 计算和 schedule 函数都是用 Python 编写的。Python 的 `pyton/tvm/relay/op/op.py` 中提供 `OpStrategy` 类。它只有一个API,即为策略添加一个实现: + +``` python +def add_implementation(self, compute, schedule, name="default", plevel=10) +``` + +接下来以 `topk` 为例,说明如何编写 `FTVMStrategy` 函数: + +``` python +# 添加到 python/tvm/relay/op/strategy/generic.py +@override_native_generic_func("topk_strategy") +def topk_strategy(attrs, inputs, out_type, target): + strategy = _op.OpStrategy() + strategy.add_implementation( + wrap_compute_topk(topi.topk), + wrap_topi_schedule(topi.generic.schedule_topk), + name="topk.generic") + return strategy + +# 添加到 python/tvm/relay/op/strategy 中的每个目标文件,例如 x86.py、cuda.py 等。 +@topk_strategy.register(["cuda", "gpu"]) +def topk_strategy_cuda(attrs, inputs, out_type, target): + strategy = _op.OpStrategy() + strategy.add_implementation( + wrap_compute_my_new_op(topi.cuda.topk), + wrap_topi_schedule(topi.cuda.schedule_topk), + name="topk.cuda") + return strategy +``` + +本例使用 `topi.cuda.topk` 和 `topi.cuda.schedule_topk` 作为 CUDA 或 GPU target 的 compute 和 schedule 函数,而对其余 target 使用 TOPI 通用计算和 schedule。 + +注意,用两个包装函数来包装 topi compute 和 schedule,使其符合所需的函数签名(参阅 `include/tvm/relay/op_attr_types.h` 中的 `FTVMCompute` 和 `FTVMSchedule`)。通常需要为每个算子编写一个自定义的计算包装函数,来获取 op 属性中不同的字段。 + +以上例子展示了一个非常基本的策略函数,它只在策略中添加了一个实现。但是对于许多复杂的算子而言,可能需要添加使用不同算法的多个实现。例如,可以同时使用 direct 算法和 winograd 算法来计算 conv2d 操作。编写如下策略函数来实现: + +``` python +strategy.add_implementation( + wrap_compute_conv2d(topi.cuda.conv2d_nchw), + wrap_topi_schedule(topi.cuda.schedule_conv2d_nchw), + name="conv2d_nchw.cuda", + plevel=10) + +if winograd_condition: + strategy.add_implementation( + wrap_compute_conv2d(topi.cuda.conv2d_nchw_winograd), + wrap_topi_schedule(topi.cuda.schedule_conv2d_nchw_winograd), + name="conv2d_nchw_winograd.cuda", + plevel=15) +``` + +这个例子中,我们向 conv2d 策略添加了两个实现,其中 winograd 算法仅在 `winograd_condition` 为 True 时添加。当 winograd_condition 为 True 时,`“conv2d_nchw_winograd.cuda”` 实现用于编译 conv2d,因为它具有更高的优先级(AutoTVM 模板的实现可以更改此设置。详细信息参阅 [从 op 策略中选择实现](#select-implementation-from-op-strategy))。否则,使用 `“conv2d_nchw.cuda”`。 + +可以将上面的示例扩展到第三方库实现。例如,当 cblas 包含在 target 中时,可以在 cblas 库中添加调用内核的实现。 + +``` python +if "cblas" in target.libs: + strategy.add_implementation( + wrap_compute_dense(topi.x86.dense_cblas), + wrap_topi_schedule(topi.x86.schedule_dense_cblas), + name="dense_cblas.x86", + plevel=15) +``` + +此外,可以添加针对特定 shape 范围的实现。以下代码显示了一个密集策略示例,该示例添加了一个专门用于 `m` > 16 的实现。硬编码 Python 条件(如上例所示)和特定条件之间的主要区别在于,当输入张量具有符号特征的 shapes 时,前者允许 TVM 生成多个内核 shape。编译引擎会生成一个 dispatch 函数,当满足相应条件时会调用专门的内核;否则,调用没有关联特殊条件的内核(本例中为 `dense_common`)。这部分仍在开发中。完成后将提供更多详细信息。 + +``` python +def dense_strategy(attrs, inputs, out_type, target): + m = inputs[0].shape[0] + strategy = _op.OpStrategy() + strategy.add_implementation( + wrap_compute_dense(dense_compute1), + wrap_topi_schedule(dense_schedule1), + name="dense_common") + + with tvm.te.SpecializedCondition(m > 16): + strategy.add_implementation( + wrap_compute_dense(dense_compute2), + wrap_topi_schedule(dense_schedule2), + name="dense_for_large_m", + plevel=15) + + return strategy +``` + +## 将策略函数注册到算子 + +定义了一个算子的策略函数之后,可以将策略函数注册到这个算子 + +``` python +register_strategy("topk", strategy.topk_strategy) +``` + +但是,为算子编写策略函数需要花费很多精力。因此,我们提供了两种更简单的方法。 + +第一种方法是,对于具有单射、广播或归约模式的算子,可以分别调用 `register_injective_schedule`、`register_broadcast_schedule` 和 `register_reduce_schedule`。每个 target 都已经注册了这些模式的 schedule 函数,并且可以应用于这些算子。假设所有 target 上的 compute 函数都是相同的,并且 `FTVMCompute` 要在调用注册 schedule 前注册到 op。 + +``` python +register_broadcast_schedule("add") +``` + +第二种方法是,对于不具备前面提到的这些常见模式,但对所有 targets 具有相同 compute 函数的算子,可以使用 `register_schedule` API。`FTVMSchedule` 函数更容易编写,因为只需提供要用的 schedule 函数即可。以下代码片段展示了用于池化的 `FTVMSchedule` 函数。 + +``` python +# 添加到 python/tvm/relay/op/strategy/generic.py +@generic_func +def schedule_pool(attrs, outs, target): + with target: + return topi.generic.schedule_pool(outs, attrs.layout) + +# 添加到 python/tvm/relay/op/strategy 中的每个目标文件,例如 x86.py、cuda.py 等。 +@schedule_pool.register("cpu") +def schedule_pool_cpu(attrs, outs, target): + ... +``` + +为算子创建 `FTVMSchedule` 后,可以使用 `register_schedule` 注册策略: + +``` python +register_schedule("nn.max_pool2d", strategy.schedule_pool) +``` + +## 为新 Target 注册策略 + +有两种方法可以为新 target 注册策略。更直接的方法是在目录 `python/tvm/relay/op/strategy` 中添加一个新的目标文件。只需为在新 target 上实现的算子自定义策略,其他的算子复用通用策略。 + +或者,也可以在 TVM Python 库之外为新 target 注册策略。以下代码片段显示了如何执行此操作的示例。`vta/python/vta/top/op.py` 中可以找到更多示例。 + +``` python +@relay.op.strategy.conv2d_strategy.register("mytarget") +def conv2d_strategy_mytarget(attrs, inputs, out_type, target): + ... +``` + +## 从 Op 策略中选择实现 + +在编译过程中,Relay 编译引擎要确定当有多个算子时,使用哪个算子来实现。选择策略的工作原理如下。 + +当算子或融合算子的输入张量都具有恒定 shape 时,编译引擎首先会根据 AutoTVM 调整日志找到最佳实现。如果没有 AutoTVM 模板的实现,或所有 AutoTVM 模板都有回退配置,则选择具有最高优先级的实现。在这种情况下,具有相同优先级的实现会导致未定义的行为,并且实现方法的选择具有随机性。 + +具有符号特征输入 shape 的算子的选择策略仍在开发中。目前,如果所有输入张量都具有符号特征的 shape,则只有具有最高优先级的实现将用于此算子。实现后将更新这部分。 + +可以在编译 Relay 模型之前添加以下代码行进行调试,来了解每个算子的实现(implementation)。 + +``` python +logging.getLogger("te_compiler").setLevel(logging.INFO) +logging.getLogger("te_compiler").addHandler(logging.StreamHandler(sys.stdout)) +``` diff --git a/versioned_docs/version-0.12.0/arch/arch/11-convert_layout.md b/versioned_docs/version-0.12.0/arch/arch/11-convert_layout.md new file mode 100644 index 00000000..14def826 --- /dev/null +++ b/versioned_docs/version-0.12.0/arch/arch/11-convert_layout.md @@ -0,0 +1,249 @@ +--- +title: Convert Layout Pass +sidebar_position: 210 +--- + +# Convert Layout Pass + +**作者:**[Animesh Jain](https://github.com/anijain2305) + +## 1. 背景 + +数据布局格式(Data layout format)描述了数据在内存中的布局方式。例如,卷积算子的 TensorFlow 框架默认数据布局是 NHWC,即数据是四维的,并以行优先格式布局,N 为第一维,C 为最后一维。 + +数据布局在模型性能中起主要作用,对空间和时间局部性有重大影响。例如,TVM 中的 Intel x86 后端往往采用 NCHWc 布局,其中 C 维度以二维形式平铺,从而有效利用数据局部性(data locality)。同样,CUDA 后端的数据布局往往采用 NCHW 格式。 + +本质上,TVM 必须处理整个编译器工具链中的数据布局——框架解析器、Relay 布局转换和 TOPI schedule。随着我们转向第三方 codegen 集成(它可能有自己的数据布局限制),处理 TVM 工具链中所有级别的布局变得更具挑战性。因此,我们开发了一个新的 Relay pass——**ConvertLayout**——减少布局处理引发的问题。 + +若要了解 ConvertLayout Pass 的用法,请直接跳到本节第 4 部分 - 用法。 + +## 2. 动机和概述 + +下面看一个简单的场景,了解由于不同布局引发的问题——假设要为 ARM 边缘设备编译一个 TensorFlow NHWC 计算图。但是,假设目前在 TOPI for ARM 中仅支持 NCHW schedule。因此,框架布局和 TOPI 支持的布局之间存在不匹配。 + +处理这种不匹配的方法之一是,在每次卷积之前和之后插入布局转换,这样得到的卷积具有 NCHW 输入数据布局,并且可以使用 TOPI schedule。但是,由于存在过多的布局转换,这可能会导致性能下降。 + +其他用例中也遇到了类似的问题 + +* 无法在 NVIDIA GPU 上运行 TFLite 计算图。 TOPI 为 GPU 提供了仅限 NCHW 的 schedules。 +* 为了支持不同的布局转换对,AlterOpLayout 用于卷积的逻辑越来越复杂。 +* 由于额外的布局转换,导致 TF 计算图的性能并非最优。 +* 第三方 codegen 集成中的复杂性,例如 TensorRT,它往往采用一种格式的数据布局。 + +为了解决这些问题,我们引入了 *ConvertLayout* pass。该 pass 设置了基础架构,它通过最小数量的数据布局转换,来更改整个计算图的数据布局。在理想情况下,只有 2 个数据布局转换,一个在开头,一个在结尾。以下是转换的示例 + +``` c++ +# 原始计算图 - NHWC 格式的 2 个卷积。 +fn (%x: Tensor[(1, 56, 56, 64), float32], %weight1: Tensor[(3, 3, 64, 32), float32], %weight2: Tensor[(3, 3, 32, 32), float32]) { + %0 = nn.conv2d(%x, %weight1, padding=[1, 1], channels=32, kernel_size=[3, 3], data_layout="NHWC", kernel_layout="HWIO"); + %1 = nn.relu(%0); + %2 = nn.conv2d(%1, %weight2, padding=[1, 1], channels=32, kernel_size=[3, 3], data_layout="NHWC", kernel_layout="HWIO"); + nn.relu(%2) +} + +# 在 ConvertLayout 之后 - 对于数据,在开始和结束时都有一个变换。 +# 对于权重,有适应 NCHW 布局的变换。这些将被 FoldConstant pass 删除。 +fn (%x: Tensor[(1, 56, 56, 64), float32], %weight1: Tensor[(3, 3, 64, 32), float32], %weight2: Tensor[(3, 3, 32, 32), float32]) { + %0 = layout_transform(%x, src_layout="NHWC", dst_layout="NCHW") /* ty=Tensor[(1, 64, 56, 56), float32] */; + %1 = layout_transform(%weight1, src_layout="HWIO", dst_layout="OIHW") /* ty=Tensor[(32, 64, 3, 3), float32] */; + %2 = nn.conv2d(%0, %1, padding=[1, 1], channels=32, kernel_size=[3, 3]) /* ty=Tensor[(1, 32, 56, 56), float32] */; + %3 = nn.relu(%2) /* ty=Tensor[(1, 32, 56, 56), float32] */; + %4 = layout_transform(%weight2, src_layout="HWIO", dst_layout="OIHW") /* ty=Tensor[(32, 32, 3, 3), float32] */; + %5 = nn.conv2d(%3, %4, padding=[1, 1], channels=32, kernel_size=[3, 3]) /* ty=Tensor[(1, 32, 56, 56), float32] */; + %6 = nn.relu(%5) /* ty=Tensor[(1, 32, 56, 56), float32] */; + layout_transform(%6, src_layout="NCHW", dst_layout="NHWC") /* ty=Tensor[(1, 56, 56, 32), float32] */ +} +``` + +## 3. 设计 + +在深入研究 ConvertLayout pass 之前,让我们根据算子对数据布局的敏感性将它们分为 3 类。此分类稍后将有助于了解 Convertlayout pass 的详细信息。 + +* **布局无关** - relu、pow 等。这些算子的功能和性能都不受数据布局的影响。 +* **轻度布局敏感** - pad、concatenate 和 reduce算子(如 sum 等)。如果在它们之前进行布局转换,这些算子的某些属性会在功能上受到影响。但是,性能的差异不是很明显。对于这些算子来说,只需要适应之前的算子输出的数据布局。 +* **重度布局敏感** - convolution、conv2d_transpose 等。这些算子在功能和性能方面都受到数据布局的严重影响。它们还具有数据布局作为 op 属性。通常,修改这些算子的输入数据布局是有益的(如果它不是高性能数据布局),而其余的*布局无关*和*轻度布局敏感*的算子会适应受*重度布局敏感*算子输出控制的布局。 + +现在来看两个相关的 Relay 算子属性。每个 Relay 算子都有属性,例如 InferType,可以由 TVM 开发者定义。通常,Relay pass 会逐个算子遍历计算图并读取这些算子属性。例如,InferType pass 查看算子的 InferType 属性,确定其输出 shape 和类型,然后将其传给下一个算子的 InferType 属性。 + +同样,在上下文中,有 2 个这样的属性——*FTVMConvertLayout* 和 *FInferCorrectLayout*。ConvertLayout pass 遍历计算图,并查看这两个属性和自动布局转换插入模块,从而处理数据布局。所以,整个过程可以分为 3 个步骤: + +* 运行 FTVMConvertLayout 属性——这允许开发者将原始 Relay expr 转换为具有新布局的新 Relay expr,从而允许用户定义的布局更改。为了方便开发者,可以借助一个 Python 回调函数(仅用于重度布局敏感算子)。 +* 运行 FTVMInferCorretLayout 属性——可以将其视为布局推理。它查看原始输入布局和新输入布局,它们要么来自先前的算子,要么来自 FTVMConvertLayout 修改后的 expr(如果已使用)。轻度布局敏感的算子可以借助它,让属性适应新的数据布局。每个算子都会有布局推理。 +* 布局转换的自动插入——上一步的布局推理为输入 expr 设置新布局。若这些布局与原始布局不同,则此组件会自动插入布局转换。因此,开发者不需要为此组件做额外操作。 + +这些步骤按顺序发生在所有算子上,其中 ConvertLayout pass 不断将新布局传给下一个算子属性,最终导致逐个算子地修改整个计算图。下面看几个定义这两个属性的示例。 + +**FTVMConvertLayout - 布局更改的 Python 回调函数** - 用于*重度布局敏感*的算子。例如,可以返回具有新数据和内核布局的新卷积算子。若需要,其他 2 个组件将推理布局,并插入布局转换。转换为 NCHW 布局的卷积算子的示例如下: + +``` python +@reg.register_convert_op_layout("nn.conv2d") +def convert_conv2d(attrs, inputs, tinfos, desired_layouts): + """为 conv2d 算子注册 Convert Layout pass。 + + 参数 + ---------- + attrs : tvm.attrs.Attrs + 当前卷积的属性 + inputs : list of tvm.relay.Expr + Relay expr 的 args 将合法化 + tinfos : list of types + 输入输出类型列表 + desired_layouts : list of layout strings + 定义我们想要的布局列表 + 分别用于数据和内核输入的布局。 + + 返回 + ------- + result : tvm.relay.Expr + 转换后的 expr + """ + + from tvm import relay + data, weight = inputs + new_attrs = dict(attrs) + + # 我们期望指定 2 个所需的布局,一个用于数据,一个用于内核。 + assert len(desired_layouts) == 2, "A desired layout is expected for both of nn.conv2d's inputs" + + # 使用指定数据布局的所需布局中的第一个条目。 + # 此算子的预期布局顺序由此函数定义。 + desired_data_layout, desired_kernel_layout = map(str, desired_layouts) + + assert desired_data_layout != "default", "Data layout cannot be default" + + new_attrs['data_layout'] = desired_data_layout + + if desired_data_layout == 'NCHW': + if desired_kernel_layout != 'default': + new_attrs['kernel_layout'] = desired_kernel_layout + else: + new_attrs['kernel_layout'] = 'OIHW' + # 布局转换的实际插入在内部进行 + # 通过 ConvertLayout pass。 + return relay.nn.conv2d(data, weight, **new_attrs) + + raise ValueError('Layout %s is not yet supported' % desired_data_layout) +``` + +**FInferCorrectLayout - 布局推理** - 目前,此属性仅在 C++ 中公开。此函数采用原始输入布局和新输入布局(由前一个算子传递,或由用于布局更改的 Python 回调函数传递),并推理最终数据布局。每个算子都会调用布局推理。算子类别不同,其使用也会有所不同。 + +对于布局无关的算子,只要返回这个函数中新的数据布局。对于轻度布局和重度布局敏感的算子,可以更改算子属性(如用于连接的轴,和用于填充的 pad_width),以便适应新的数据布局,防止插入布局转换。下面看几个例子来更好地理解这一点。 + +第一个例子是布局无关的算子。这些算子没有属性受数据布局影响,所以只适应新的布局。 + +``` c++ +// 设置算子属性,如下所示 +// .set_attr("FInferCorrectLayout", ElemwiseArbitraryLayout); + +// 采用任意输入布局,并复制到输出。 +inline Array > ElemwiseArbitraryLayout(const Attrs& attrs, + const Array& new_in_layouts, + const Array& old_in_layouts, + const Array> &old_in_shapes) { + Layout ret; + + if (new_in_layouts.defined()) { + ICHECK_GE(new_in_layouts.size(), 1); + ret = new_in_layouts[0]; + } else { + for (size_t i = 0; i < old_in_layouts.size(); ++i) { + if (old_in_layouts[i].defined()) { + ret = old_in_layouts[i]; + break; + } + } + } + + return Array >{Array(old_in_layouts.size(), ret), {ret}}; +} +``` + +第二个示例适用于轻度布局敏感的算子——batch normalization。若从 NHWC 转到 NCHW 数据布局时,BatchNorm 的 axis 算子必须更改。 (重度布局敏感的算子处理类似。) + +``` c++ +Array> BatchNormInferCorrectLayout(const Attrs& attrs, + const Array& new_in_layouts, + const Array& old_in_layouts, + const Array>& old_in_shapes) { + BatchNormAttrs* param = const_cast(attrs.as()); + + size_t axis = + param->axis < 0 ? param->axis + old_in_shapes[0].size() : static_cast(param->axis); + + Layout ret = Layout::Undef(); + + // 例如,考虑 old_layout = NHWC, and new_layout = NCHW, and param->axis = 3 + if (new_in_layouts.defined() && old_in_layouts.defined()) { + // 获取新的 C 轴。提取旧布局中的 dim。在下一个布局中找到该 dim 的索引。 + + // 以下代码给出 bn_dim = C as old_layout = NHWC, axis = 3 + const auto& bn_dim = old_in_layouts[0][axis]; + + // new_index 为 1,因为 new_layout = NCHW 且 bn_dim 为 C + auto new_index = new_in_layouts[0].IndexOf(bn_dim); + + // 修改 layout-dependent 属性 - axis 为 1。 + param->axis = new_index; + + // 最后,适应新的布局。 + ret = new_in_layouts[0]; + + } else if (old_in_layouts.defined()) { + ret = old_in_layouts[0]; + } + + // 如果新旧布局都未定义,则无需更改。 + // 在这种情况下,ConvertLayout pass 会跳过布局转换的自动插入。 + + // 以下代码对教程并不重要。但是,布局推理需要定义 + // 所有输入和输出数据布局的布局。对于 batch norm,其他输入 + // 并且输出是输入中长度为 C dim 的向量。所以,我们设置另一个 + // 布局为 C。BN 有 5 个输入,3 个输出。最后 4 个输入和最后 2 个输出 + // 有「C」布局。 + Layout c_layout = Layout("C"); + + return Array>{{ret, c_layout, c_layout, c_layout, c_layout}, + {ret, c_layout, c_layout}}; +} +``` + +## 4. 用法 + +ConvertLayout pass 非常易于使用。pass 不是默认 relay.build pipeline 的一部分。预期用途是在 framework-to-relay 解析器和 relay.build 模块调用之间调用它。 + +为了指定要转换到的布局,创建一个映射,该映射由重度布局敏感的算子指向该算子期望布局的列表。以下第一个示例指定了数据布局,允许内核布局自动转换为 TVM 支持的布局(针对特定的数据布局和算子)。这是通过使用「default」关键字指定的。 + +第二个示例显示了如何转换为指定内核布局。注意,以下示例将转换为相同的布局,即 `{‘nn.conv2d’: [‘NCHW’, ‘default’]} == {‘nn.conv2d’: [‘NCHW’, ‘OIHW’]}` + +``` python +# TFlite 框架到 Relay 解析器 - 默认布局是 NHWC +mod, params = relay.frontend.from_tflite(tflite_model, + shape_dict=shape_dict, + dtype_dict=dtype_dict) + +# 假设模型的重度布局敏感算子仅包含 nn.conv2d +desired_layouts = {'nn.conv2d': ['NCHW', 'default']} + +# 将布局转换为 NCHW +# RemoveUnunsedFunctions 用于清理计算图。 +seq = tvm.transform.Sequential([relay.transform.RemoveUnusedFunctions(), + relay.transform.ConvertLayout(desired_layouts)]) +with tvm.transform.PassContext(opt_level=3): + mod = seq(mod) + +# 调用 Relay 编译 +with relay.build_config(opt_level=3): + graph, lib, params = relay.build(mod, target, params=params) +``` + +``` python +desired_layouts = {'nn.conv2d': ['NCHW', 'OIHW']} +pass = relay.transform.ConvertLayout(desired_layouts) +``` + +布局的顺序由 register_convert_op_layout(“OPNAME”) 的实现定义,可以参考明确说明了预期布局的文档字符串。以上示例中是 [data_layout, kernel_layout]。 + +当前的实现几乎支持图像分类模型中所有常用算子。但是,如果在计算图中遇到太多数据布局转换,则很可能存在个别算子,需要特殊处理其布局,如第 3 节所述。在这种情况下可以参考的 pull requests 是 + +* [Batch Norm](https://github.com/apache/tvm/pull/4600) 的布局推理 - Batch normalization 属于轻度敏感算子。 PR 展示了如何处理 batch norm 的布局推理。 +* [Convolution](https://github.com/apache/tvm/pull/4335) Python 回调函数 - 对于重度敏感的算子,还需要执行 Python 回调函数。 PR 展示了如何为卷积算子定义 Python 回调函数。 diff --git a/versioned_docs/version-0.12.0/arch/arch/12-benchmark.md b/versioned_docs/version-0.12.0/arch/arch/12-benchmark.md new file mode 100644 index 00000000..771cc3d0 --- /dev/null +++ b/versioned_docs/version-0.12.0/arch/arch/12-benchmark.md @@ -0,0 +1,101 @@ +--- +title: Benchmark 性能日志格式 +sidebar_position: 220 +--- + +# Benchmark 性能日志格式 + +本节详细介绍了统一 benchmark 日志格式的架构 v0.1。该模式使得交叉引用更容易,包括与其他框架/运行、实验再现、每日性能回归数据,以及日志记录/可视化工作的分离。 + +## 日志格式概述 + +简单起见,建议优先处理 *workload*,*engine*,*hardware runtime_ms_mean* 和 *runtime_ms_std* 字段。更细粒度的日志记录,可以另外传播 **_config* 字段。 + +| **header** | **examples** | **category** | **notes/justification** | +|:---|:---|:---|:---| +| workload | resnet-18 | workload | name of workload | +| engine | “tvm” / “onnxruntime” | compiler | | +| hardware | “gcp-c2-standard-16” | hardware | descriptor of target hardware environment | +| runtime_ms_mean | 12.452 | statistics | | +| runtime_ms_std | 5.3 | statistics | | +| timestamp | 1572282699.6 | metadata | indicates when this record is logged | +| schema_version | “0.1” | metadata | ensure reproducibility as we iterate on this schema | +| metadata | `{ “docker_tag”:”gcr.io/…/0a680”, … }` | metadata | docker_tag is optional | +| workload_args | `{“input_name”: “Input3”, “input_shape”: [list_of_shape], “data_layout”: NHCW}` | workload | | +| workload_metadata | `{“class”: “vision”,”doc_url”: “https://github.com/.../README.md”, “opset”: 7,”type”: “body_analysis”,”url”: “https://onnxzoo...ferplus.tar.gz”, “md5”: “07fc7…”}` | workload | source of workload | +| engine_version | “1.0.5” | compiler | use semvar format | +| engine_config | `{“llvm”: “llvm-8”, “nvcc”: 10.1, “accelerator”: “MLAS”, “relay_opt_level”: 3, “tvm_target”:”llvm -mcpu=cascadelake”}` | compiler | fields are optionally specified | +| compilation_config | `{“opt_level”: 3, “layer_schedules”:[]/ }` | compiler | fields are optionally specified | +| software_config | `{“os”: “ubuntu:18.04”,”pip”: { “docker”: “4.1.0”, “gitpython”: “3.0.4”, “numpy”: “1.17.4”, “onnx”: “1.6.0”}, “cudnn”: “cudnn-8”, “cuda_driver”: “480.10.1”}` | backend | env dependency list | +| runtime_config | `{“num_cpu_threads”: 3}` | backend | info on non-hardware, non-software metadata | +| hardware_config | `{“cpu_count”: 16, “cloud_machine_type”:”c2-standard-16”, “memory_GB”:64}` | hardware | json descriptor of target hardware environment | +| execution_config | `{“number”: 1, “repeat”: 10, “min_repeat_ms”, 0}` | statistics | workload execution parameters | +| metrics | `{“accuracy”: 48.5,“compilation_ms_mean”: 12}` | statistics | other metrics | +| runtime_raw | `[{“runtime_ms”: 12, …}, {“runtime_ms”:13,…},…]` | statistics | optional raw metrics array | + +## 存储格式 + +目前,为了可扩展性和便利性,正在将 benchmark 数据原型化为 JSON 对象,尤其是在模式的早期版本中。但是,随着我们扩大 benchmark 聚合,并稳定参数,预计会切换成列格式,例如 Arrow 或 Parquet。 + +以下是编码为 JSON 的示例数据: + +``` json +{ + "workload":"arcface_resnet100", + "engine":"tvm", + "hardware":"gcp-c2-standard-16", + "runtime_ms_mean":109.43004820081924, + "runtime_ms_std":0.09078385126800587, + "timestamp":"20191123003411", + "schema_version":"0.1", + "metadata":{ + "docker_tag":"tlcpack/ci-gpu:v0.53" + }, + "workload_args":{ + "input_shape_dict":{ + "data":[ + 1, + 3, + 112, + 112 + ] + }, + "input_type_dict":{ + "data":"float32" + }, + "input_value_dict":{} + }, + "workload_metadata":{ + "class":"vision", + "doc_url":"https://github.com/onnx/models/blob/main/vision/body_analysis/arcface/README.md", + "md5":"66074b860f905295aab5a842be57f37d", + "opset":8, + "type":"body_analysis", + "url":"https://s3.amazonaws.com/onnx-model-zoo/arcface/resnet100/resnet100.tar.gz" + }, + "engine_version":"1.0.0", + "engine_config":{}, + "compilation_config":{ + "relay_opt_level": 3 + }, + "software_config":{ + "os":"ubuntu:18.04", + "pip":{ + "docker":"4.1.0", + "gitpython":"3.0.4", + "numpy":"1.17.4", + "onnx":"1.6.0" + } + }, + "runtime_config":{}, + "hardware_config":{ + "cloud_machine_type":"c2-standard-16", + "cloud_provider":"GCP", + "cpu_count":16, + "cpu_platform":"Intel Cascade Lake", + "memory_GB":64 + }, + "execution_config":{}, + "metrics":{} +} +``` diff --git a/versioned_docs/version-0.12.0/arch/arch/13-tensorflow.md b/versioned_docs/version-0.12.0/arch/arch/13-tensorflow.md new file mode 100644 index 00000000..58878b1a --- /dev/null +++ b/versioned_docs/version-0.12.0/arch/arch/13-tensorflow.md @@ -0,0 +1,227 @@ +--- +title: TensorFlow 前端 +sidebar_position: 230 +--- + +# TensorFlow 前端 + +TensorFlow 前端有助于将 TensorFlow 模型导入 TVM。 + +支持的版本: + +* 1.12 及以下 + +测试模型: + +* Inception(V1/V2/V3/V4) +* Resnet(全部) +* Mobilenet(V1/V2 全部) +* Vgg(16/19) +* BERT(基础/3 层) + +## 为推理准备模型 + +### 删除不需要的节点 + +导出过程将删除许多不需要的推理节点,但还是会留下一些。需要手动删除的节点有: + +* Dropout,包括 [Dropout](https://www.tensorflow.org/api_docs/python/tf/nn/dropout) 和 [DropoutWrapper](https://www.tensorflow.org/versions/r1.12/api_docs/python/tf/nn/rnn_cell/DropoutWrapper?hl=hr) +* [Assert](https://www.tensorflow.org/api_docs/python/tf/debugging/Assert) + +### 将 None Dimensions 转换为常量 + +TVM 对动态张量 shape 的支持最少。为 `None` 的维度应替换为常量。例如,模型可以接受 shape 为 `(None,20)` 的输入。这应该转换为类似 `(1,20)` 的 shape。应相应地修改模型以确保这些 shape 在整个计算图中匹配。 + +### 导出 + +TensorFlow 前端需要一个冻结的 protobuf(.pb)或保存的模型作为输入。它目前不支持检查点(.ckpt)。TensorFlow 前端所需的 graphdef 可以从活动 session 中提取,或者使用 [TFParser](https://github.com/apache/tvm/blob/main/python/tvm/relay/frontend/tensorflow_parser.py) 辅助类。 + +导出模型时应进行一些转换,以准备模型进行推理。设置 `add_shapes=True` 也很重要,因为这会将每个节点的输出 shape 嵌入到图中。这是一个将模型导出为给定会话的 protobuf 的函数: + +``` python +import tensorflow as tf +from tensorflow.tools.graph_transforms import TransformGraph + +def export_pb(session): + with tf.gfile.GFile("myexportedmodel.pb", "wb") as f: + inputs = ["myinput1", "myinput2"] # 替换为你的输入名称 + + outputs = ["myoutput1"] # 替换为你的输出名称 + graph_def = session.graph.as_graph_def(add_shapes=True) + graph_def = tf.graph.util.convert_variables_to_constants(session, graph_def, outputs) + graph_def = TransformGraph( + graph_def, + inputs, + outputs, + [ + "remove_nodes(op=Identity, op=CheckNumerics, op=StopGradient)", + "sort_by_execution_order", # 每次转换后按执行顺序排序,以确保正确的节点排序 + "remove_attribute(attribute_name=_XlaSeparateCompiledGradients)", + "remove_attribute(attribute_name=_XlaCompile)", + "remove_attribute(attribute_name=_XlaScope)", + "sort_by_execution_order", + "remove_device", + "sort_by_execution_order", + "fold_batch_norms", + "sort_by_execution_order", + "fold_old_batch_norms", + "sort_by_execution_order" + ] + ) + f.write(graph_def.SerializeToString()) +``` + +另一种方法是 [导出并冻结计算图](https://github.com/tensorflow/models/tree/master/research/slim#exporting-the-inference-graph)。 + +## 导入模型 + +### 显式 shape: + +为确保可以在整个计算图中了解 shape,将 `shape` 参数传递给 `from_tensorflow`。该字典将输入名称映射到输入 shape。有关示例,参阅这些 [测试用例](https://github.com/apache/tvm/blob/main/tests/python/frontend/tensorflow/test_forward.py#L36)。 + +### 数据布局 + +大多数 TensorFlow 模型都是使用 NHWC 布局发布的。NCHW 布局通常提供更好的性能,尤其是在 GPU 上。TensorFlow 前端可以通过将参数 `layout='NCHW'` 传递给 `from_tensorflow` 来自动转换模型的数据布局。 + +## 最佳实践 + +* 使用静态张量 shape,而非动态 shape(删除「None」尺寸)。 +* 使用静态 RNN,而非动态 RNN,因为尚不支持 `TensorArray`。 + +## 支持的算子 + +* Abs +* Add +* AddN +* All +* Any +* ArgMax +* ArgMin +* AvgPool +* BatchMatMul +* BatchMatMulV2 +* BatchNormWithGlobalNormalization +* BatchToSpaceND +* BiasAdd +* BroadcastTo +* Cast +* Ceil +* CheckNumerics +* ClipByValue +* Concat +* ConcatV2 +* Conv2D +* Cos +* Tan +* CropAndResize +* DecodeJpeg +* DepthwiseConv2dNative +* DepthToSpace +* Dilation2D +* Equal +* Elu +* Enter +* Erf +* Exit +* Exp +* ExpandDims +* Fill +* Floor +* FloorDiv +* FloorMod +* FusedBatchNorm +* FusedBatchNormV2 +* Gather +* GatherNd +* GatherV2 +* Greater +* GreaterEqual +* Identity +* IsFinite +* IsInf +* IsNan +* LeakyRelu +* LeftShift +* Less +* LessEqual +* Log +* Log1p +* LoopCond +* LogicalAnd +* LogicalOr +* LogicalNot +* LogSoftmax +* LRN +* LSTMBlockCell +* MatMul +* Max +* MaxPool +* Maximum +* Mean +* Merge +* Min +* Minimum +* MirrorPad +* Mod +* Mul +* Neg +* NextIteration +* NotEqual +* OneHot +* Pack +* Pad +* PadV2 +* Pow +* Prod +* Range +* Rank +* RealDiv +* Relu +* Relu6 +* Reshape +* ResizeBilinear +* ResizeBicubic +* ResizeNearestNeighbor +* ReverseV2 +* RightShift +* Round +* Rsqrt +* Select +* Selu +* Shape +* Sigmoid +* Sign +* Sin +* Size +* Slice +* Softmax +* Softplus +* SpaceToBatchND +* SpaceToDepth, +* Split +* SplitV +* Sqrt +* Square +* SquareDifference +* Squeeze +* StridedSlice +* Sub +* Sum +* Switch +* Tanh +* TensorArrayV3 +* TensorArrayScatterV3 +* TensorArrayGatherV3 +* TensorArraySizeV3 +* TensorArrayWriteV3 +* TensorArrayReadV3 +* TensorArraySplitV3 +* TensorArrayConcatV3 +* Tile +* TopKV2 +* Transpose +* TruncateMod +* Unpack +* UnravelIndex +* Where +* ZerosLike diff --git a/versioned_docs/version-0.12.0/arch/arch/14-security.md b/versioned_docs/version-0.12.0/arch/arch/14-security.md new file mode 100644 index 00000000..89ac60e2 --- /dev/null +++ b/versioned_docs/version-0.12.0/arch/arch/14-security.md @@ -0,0 +1,18 @@ +--- +title: 安全指南 +sidebar_position: 240 +--- + +## 报告安全问题 + +Apache 软件基金会非常重视软件安全及服务攻击问题。推荐先向安全邮件列表报告此类问题,然后再在公共论坛上披露这些问题。 + +注意,安全邮件列表只能用来报告未公开的安全漏洞,以及管理修复此类漏洞的过程。不能向这个地址报告常规错误或查询其他,一切与源代码中未公开的安全问题无关的邮件都将被忽略。关于以下方面的问题:如果你特殊的应用程序遇到了漏洞,并且从已发布的漏洞补丁和/或新版本里可以获取更多信息,则应发送至用户论坛进行讨论。 + +安全邮件地址:[security@apache.org](mailto:security@apache.org)。可查阅 [Apache 安全指南](https://www.apache.org/security/)。 + +## 注意事项 + +TVM 生成的默认二进制文件仅依赖最小 runtime API。runtime 依赖系统库中有限的系统调用(例如 malloc)。 + +跟踪器、服务器和客户端之间的 AutoTVM 数据交换是纯文本的。建议在可信网络环境或加密通道下使用。 \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/arch/arch/15-microtvm_design.md b/versioned_docs/version-0.12.0/arch/arch/15-microtvm_design.md new file mode 100644 index 00000000..61889377 --- /dev/null +++ b/versioned_docs/version-0.12.0/arch/arch/15-microtvm_design.md @@ -0,0 +1,233 @@ +--- +title: microTVM 设计文档 +sidebar_position: 250 +--- + +# microTVM 设计文档 + +## 背景 + +TVM 是一个模型部署框架,它在传统操作系统上的各种模型中性能较好。TVM 的分层编译方法是针对裸机设备的自然扩展。虽然大多数编译流程无需更改这类设备上的概念验证(proof-of-concept, POC)的实现,但 runtime 不能依赖于: + +* **虚拟内存**,以及任何系统提供的 `malloc`。此外,裸机设备的内存通常非常有限(以 KB 为单位)。正因如此,这类平台的库在使用内存时要更加谨慎,并且在不使用时释放内存。 +* 传统的操作系统抽象,例如 **文件**,**库** 和 **内核函数**。一些项目支持这些,但它们不是标准的。 +* 支持除 **C** 外的编程语言。 + +这类更改需要不同于传统的操作系统上的 TVM C++ runtime 的方法。 + +## 典型用途 + +本节讨论对「典型」microTVM 用例的看法。所有实现此典型用例的组件都很灵活,但这种统一的看法有助于激发每个部分的设计。 + +![/img/docs/tlc-pack/web-data/main/images/dev/microtvm_workflow.svg](/img/docs/tlc-pack/web-data/main/images/dev/microtvm_workflow.svg) + +该过程的各个部分描述如下: + +1. **模型导入**。用户导入已有模型,或向 TVM 描述新模型,生成 *Relay 模块*。 +2. **模型转换**。用户可以对模型应用变换,例如量化。每次转换后,用户仍保留 Relay 模块。 +3. **编译**(调度和代码生成)。TVM 通过为每个 Relay 算子指定 schedule 和 schedule 配置,将每个算子实现到 Tensor IR 中。然后,为每个算子生成代码(C 源代码或编译对象)。 +4. **集成**。将生成的代码与 TVM C Runtime 库一起集成到用户提供的二进制项目中。在某些情况下(例如当项目跨多个 SoC/开发单板标准化时),此过程将会自动处理。 +5. **部署**。项目已构建,剩余的固件二进制文件将烧录到设备上。模型推理由 TVM 用设备上的 RPC 服务器驱动,或者用图执行器在设备上驱动。 + +## 设计目标 + +microTVM 旨在实现以下设计目标: + +1. **可移植代码**。microTVM 可将所有 Relay 模型,转换为仅用 C 标准库就可以编译的 C 代码。 +2. **最小开销**。microTVM 生成特定于 target 的高度优化代码。应该避免 runtime 尽可能多的开销。 +3. **易懂的代码**。microTVM 将 C 源代码视为一流的输出机制,以便固件工程师更容易理解和调整。 + +## 概述 + +microTVM 要在 TVM 编译器堆栈的所有级别上进行更改。以下小节列举了高级别的变化,后续部分将更详细地讨论这些细节。 + +### 对 Target 平台建模 + +TVM 基于搜索的优化方法使其避免了对 targets 进行系统级建模,从而支持实验结果。然而,有一些建模是必要的,它们可以确保 TVM 比较的是同类搜索结果,并避免在搜索过程中,由于为 target 编译无效代码,而浪费时间。 + +microTVM 对 target 的这些部分进行建模: + +* 使用的 CPU,通过 `-mcpu` 和 `-march` target 标志。 +* 加速器的存在与否,通过 target 的设备组件(目前只能表示加速器的缺失,但这种机制有待进行更好地扩展)。 + +microTVM 未来要对 target 的这些部分进行建模: + +* 内存,建模为一组不相交的内存空间,每个空间都有一个标签和大小,以及预取/刷新行为。一些内存会与加速器共享空间。 +* Target runtime 配置(即时钟树配置、时钟速度等)。它仅用于 AutoTVM schedule 密钥,不用于任何其他用途。 + +目前,TVM 不建模的部分: + +* 缓存的大小、类型或关系,除预取或缓存刷新外。 + +### microTVM 的 TVM Target + +编译过程的中心数据结构是 `tvm::target::Target` 类。 TVM 用 Target 来决定启用哪些 TIR schedules,以及如何配置代码生成器。Target 类还应该唯一地标识为特定算子生成的代码,因为自动调优日志用它来对测试的性能进行排名(参阅未来工作)。 + +Targets 当前表示为字符串,其结构类似于命令行参数。Targets 示例如下所示: + +``` plain +c -keys=arm_cpu -mcpu=cortex-m7 -model=stm32f746xx +``` + +microTVM 的相关部分是: + +* 代码生成器(`llvm` 或 `c`) +* `-mcpu=cortex-m7`:TOPI 用来启用 Cortex-M schedules,并且,当选择 C 源代码生成器时,将其作为注释包含在输出中,从而有利于识别代码,并配置下游 C 编译器。 + +### microTVM 的 Runtime 和执行器配置 + +使用 microTVM 时,会用 C Runtime(`Runtime('crt')`)很重要,它是最适合在微型设备上运行的 Runtime,而非更动态的 C++ Runtime。此外,还有两个执行器可以与 C Runtime 结合使用: + +* `Executor("aot")` - Ahead of Time(AOT)执行器将网络预编译成一个可运行的函数,然后将其直接添加到微应用程序中 +* `Executor("graph", {"link-params": True})` - 图执行器提供了网络的 JSON 表示,并且需要生成 C Runtime 的系统库,从而在函数注册表(`Runtime("crt" , {"system-lib": True})`)中查找函数。`{"link-params":True}` 允许将参数链接到生成的文件,而非从外部提供。 + +这些是在构建 runtime 模块时指定的:`relay.build(..., runtime=..., executor=...)`。 + +### 为 microTVM 编写 Schedules + +对于在 CPU 上调度的操作,microTVM 最初计划利用专门的指令和外部(即手动优化)函数来提高性能。在 TVM 中,这种方法通常是通过张量实现的——TVM 将计算分解为多个部分,而 TIR 外部函数会加速每个部分。 + +TVM 目前用 `tir.call_extern` 来适应这两种方法。首先,将 pragma 附加到 schedule 上,这个 schedule 定义了可移植 C 代码中的外部函数。 + +``` plain +sched[output].pragma(n, "import_c", "void call_asm(int32_t* a, int32_t* b) { /* ... */ }") +``` + +接下来,用 `tensorize` 来拆分计算 + +``` plain +sched[output].tensorize(owi, gemm) +``` + +这种方法有几个注意事项,都可以通过链接生成代码与外部库来解决: + +* 内联汇编是特定于编译器的。虽然 Clang 和 GCC 已经对一种语法进行了标准化,但可能无法移植到其他编译器。SDK 根据使用的编译器,有条件地包含一个头文件来解决这个问题。但是,采用这种方法意味着生成的代码需要额外的编译器标志(即 `-Isystempath/to/header`)。 +* 引用生成的代码中的辅助函数会很有用(例如,手动优化汇编的内联通用序列)。 +* 最后,调用的外部函数可以完全写在外部库中。若这些函数可以完全内联,则警告与前面的相同。若不是,则需要编译额外的 C 代码,并将其链接到算子。 + +目前,microTVM 假定所有符合条件的 schedules 都可以编译。这意味着用户提供的项目(参见下一节)必须包含生成的代码使用的所有库。 + +不使用自动调优时,TVM 随机选择一个回调 schedule,因此要支持所有库。使用自动调优时,TVM 会选择性能最佳的 schedule,因此只需要该库。目前没有办法强制 TVM 选择自动调优日志外的特定的 schedule,未来将考虑增加该功能。 + +最后,使用 `llvm` 后端时,除了 LLVM 位码包含在生成的代码中(使用 `import_llvm` pragma)之外,这个过程是相似的。LLVM 位码提供了一种调用内联汇编的可移植方式。但是,调用外部 C 函数更复杂,在 LLVM 位码中使用辅助函数也不容易。 + +### 执行模型 + +TVM 编译器通常会输出三个部分: + +1. 如上所述的模型算子实现; +2. 模型执行图,编码为 JSON; +3. 简化参数。 + +为了能够正确执行模型,图执行器要在内存中重建计算图,加载参数,然后以正确的顺序调用算子的实现。 + +microTVM 支持两种方式: + +1. **主机驱动**。 图执行器可以在主机上运行并通过使用带有类似 UART 传输的 RPC 链接向设备发出命令来执行。 +2. **脱机执行**。C 图执行器可用于在设备上编译,但它的内存效率不是特别高。这种方式不依附于主机独立执行。 + +主机驱动的方法用于在设备上试验模型,类似 AutoTVM 用 RPC 服务器来驱动设备上的计算。脱机执行的方式用于部署。 + +#### 主机驱动执行 + +在主机驱动执行中,固件二进制如下: + +1. 从 TVM 生成的算子实现。 +2. TVM C runtime。 +3. 特定于 SoC 的初始化。 +4. TVM RPC 服务器。 +5. (可选)简化参数。 + +将这个固件镜像烧录到设备上,并在主机上创建一个 GraphExecutor 实例。GraphExecutor 通过 UART 发送 RPC 命令来驱动执行: + +![/img/docs/tlc-pack/web-data/main/images/dev/microtvm_host_driven.svg](/img/docs/tlc-pack/web-data/main/images/dev/microtvm_host_driven.svg) + +#### 脱机执行 + +在脱机执行中,GraphExecutor 在设备上进行实例化: + +![/img/docs/tlc-pack/web-data/main/images/dev/microtvm_standalone.svg](/img/docs/tlc-pack/web-data/main/images/dev/microtvm_standalone.svg) + +### microTVM 固件 + +接下来讨论 microTVM 固件。两种模型执行策略都有一项重要任务,即配置 SoC,从而匹配其在生产中的执行方式。 microTVM 认为此任务依赖于项目和 SoC。无论是 AutoTVM,主机驱动的模型推理,还是脱机部署,用户都希望项目中的 main() 执行: + +1. 配置 SoC,匹配部署性能。 +2. 初始化 TVM C Runtime。 + +配置主机驱动的推理或 AutoTVM 时,其余任务: + +3. 初始化传输(即 UART),用于 TVM RPC 服务器。 +4. 启动 TVM RPC 服务器。 + +配置脱机部署时,固件需要: + +1. 通过调用 `runtime.SystemLib` PackedFunc 来实例化系统库。 +2. 实例化一个 GraphExecutor,来传递系统库模块。 +3. 根据需要配置参数和输入。 +4. 运行模型。 + +### microTVM 二进制文件部分 + +总之,microTVM 固件二进制镜像必须包含: + +1. 算子实现,由 TVM 产生。 +2. TVM C runtime 库,作为静态库由 TVM 提供。 +3. SoC 初始化,由用户提供。 + +对于主机驱动的模型执行,固件还需要: + +4. TVM RPC 服务器库。 + +对于脱机模型执行,固件还需要: + +5. TVM C GraphExecutor 库,作为静态库由 TVM 提供。 +6. 其余编译器输出(简化参数和图形JSON)。 + +### 自动化构建流程 + +代码生成后,`tvm.relay.build` 返回一个 `tvm.runtime.Module`,用户可以将生成的 C 源代码或二进制对象保存到 `.c` 或 `.o` 文件中。从这一点来看,TVM 理论上可以退后一步,用户可以分开编译和运行代码。 + +但是,对于 AutoTVM,TVM 需要一些自动化流程来处理: + +1. 将算子实现、TVM C Runtime 库和 TVM RPC 服务器库集成到固件项目中,这个固件项目包含用户提供的 SoC 初始化。 +2. 构建生成的项目。 +3. 将内置固件烧录到(特定)连接的设备上。 +4. 识别 TVM 使用的串行端口或其他传输方式来驱动远程执行。 + +目前,TVM 期待用户提供 `tvm.micro.Compiler`、`tvm.micro.Flasher` 和 `tvm.micro.Transport` 接口的实现。然后 TVM: + +1. 将每个部分单独构建为一个库。 +2. 将库构建为二进制固件镜像。 +3. 将固件镜像烧录到连接的设备上。 +4. 打开一个串行端口,作为 RPC 服务器传输。 + +选择此设计是为了减少 microTVM 的构建时间(只需为每个候选算子实现构建一次公共库)。实际上,这些项目非常小,并且编译速度相对较快。与 TVM 的构建集成更紧密,导致复杂性增加。与这种增加的复杂性相比,性能提升可能不值得。未来的设计会将构建任务整合到一个步骤中,并缩小接口,从而提供更好的集成。 + +### 测试算子性能 + +TVM C runtime 依赖用户提供的函数来测试设备上的时间。用户应实现 `TVMPlatformTimerStart` 和 `TVMPlatformTimerStop`。这些函数应该测试时钟时间,因此这些函数的实现存在一些缺陷: + +1. 若 CPU 在计算期间停止或休眠(如果它正在加速器上完成),则不应该使用循环计数器,因为它们会在 CPU 休眠时停止计数。 +2. 这些函数的粒度可以根据需要放宽,以扩展定时器设备的范围。然而,若粒度太粗,则可能使用次优 schedule。 +3. 如果计时器溢出,则会引发错误。 +4. 除非绝对必要,否则计时器不应中断计算。这样做可能会影响结果的准确性。 +5. 理想的方法是根据时钟来校准输出,但可能太麻烦了。未来的 PR 可以实现平台定时器的某些特性,例如,参考外部晶体振荡器来测试内部振荡器。 + +## 未来工作 + +### Ahead-of-Time Runtime + +图执行器的限制之一是解析 JSON 所需的内存开销。当前的实现对 microTVM 的动态内存使用贡献过多,这限制了它的实用性。Ahead-of-Time Runtime 无需解析任何 Graph JSON,并且可以通过生成 C 代码来提高推理速度,这个生成的 C 代码直接调用生成的算子实现,而非依赖于图执行器的数据驱动方法。 + +### 内存规划 + +当前内存规划器仅用于限定中间张量调用 `TVMBackendDeviceAlloc()` 的次数。因为暂存器各自的差异较大,并且由于规划器将内存分配合并到彼此的 16 倍以内,所以这种策略通常会导致内存使用的高峰值出现。 + +### 异构执行 + +较新的 Cortex-M SoC 可以包含多个 CPU 和板载 ML 加速器。 + +### 自动调优 Target + +如前所述。 diff --git a/versioned_docs/version-0.12.0/arch/arch/16-microtvm_project_api.md b/versioned_docs/version-0.12.0/arch/arch/16-microtvm_project_api.md new file mode 100644 index 00000000..14be622c --- /dev/null +++ b/versioned_docs/version-0.12.0/arch/arch/16-microtvm_project_api.md @@ -0,0 +1,82 @@ +--- +title: 关于 microTVM 项目 API +sidebar_position: 260 +--- + +# 关于 microTVM 项目 API + +microTVM 项目 API 使得 TVM 在非常规或嵌入式平台上自动运行模型。它使得平台可以定义标准函数,从而将 TVM 编译器输出与样板平台特定代码集成,生成可运行的**项目**。然后,项目 API 进一步定义了构建项目的函数,以及在可从 TVM 机器获取的兼容设备上编程,并与运行代码进行通信,使得 TVM 可以执行主机驱动的推理和自动调优。 + +在许多情况下,仅需要简单地从平台的构建过程中,把 microTVM 作为工具来调用即可。事实上,对于普通的固件开发者来说,这些就足够了。但是,有几个用例需要 microTVM 用平台的构建工具来构建固件: + +1. 在平台上启用 AutoTVM 和 AutoScheduling。定义项目 API 的实现,使得 TVM 将平台上的模型调到最优的性能。 +2. 让没有固件专业知识的工程师能够在平台上试验模型。定义项目 API 的实现,使得这些工程师利用标准 TVM Python 工作流,在平台上执行主机驱动的推理。 +3. 集成测试。定义项目 API 的实现,使得能够创建持续集成测试,以验证平台上模型的正确性和性能。 + +## API 定义 + +完整的 API 是在 [python/tvm/micro/project_api/server.py](https://github.com/apache/tvm/blob/main/python/tvm/micro/project_api/server.py) 中 `ProjectAPIHandler` 上定义的 `abstractmethod`。这里不复制文档,只是简单介绍该类。 + +## TVM 如何使用项目 API + +本节介绍项目 API 如何与 TVM 一起使用。项目 API 围绕*项目*定义为固件的可构建单元。 TVM 提供一个包含*模板项目*的目录,该目录与 [模型库格式](https://tvm.apache.org/docs/arch/model_library_format.html#model-library-format) 文件一起,被构建为一个可运行的项目。 + +模板目录内(通常)是一个 Python 脚本,这个脚本实现了 API 服务器。TVM 在子进程中启动此脚本,并向服务器发送命令,执行上述操作。 + +一般的使用流程如下: + +1. 在模板项目中启动项目 API 服务器。 +2. 通过发送 `server_info_query` 命令,验证 API 服务器与 TVM 版本是否兼容,并读取实现的属性。 +3. 通过发送 `generate_project` 命令生成一个新项目。这个命令的参数是一个模型库格式和一个不存在的目录(用生成的项目填充)。模板项目 API 服务器将自身复制到新生成的项目中。 +4. 终止模板项目 API 服务器。 +5. 在生成的项目中启动项目 API 服务器。 +6. 通过发送 `server_info_query` 命令,验证 API 服务器与 TVM 版本兼容,并读取实现的属性。 +7. 通过向 API 服务器发送 `build` 和 `flash` 命令,构建和刷新项目。 +8. 与 target 通信。发送 `open_transport` 命令后,再发送 `write_transport` 和 `read_transport` 命令,从 target 的串行端口进行写入和读取操作。完成后,发送 `close_transport`。 +9. 终止项目 API 服务器。 + +## 项目的磁盘布局 + +在项目(模板或生成的)的根目录中,以下两个文件必须存在一个: + +* `microtvm_api_server.py` - (推荐方法)。将一个兼容 Python 3 的 Python 脚本放在根目录下。 TVM 会用与执行 TVM 一致的解释器,在进程中执行此脚本。 +* `microtvm_api_server.sh`(在 Windows 上,`microtvm_api_server.bat`) - (替代方法)。当需要不同的 Python 解释器时,或者想用不同的语言实现服务器时,创建这个可执行文件。 TVM 将在单独的进程中启动此文件。 + 除了这两个文件之外,对布局没有其他限制。 + +## TVM 和项目 API 服务器之间的通信 + +TVM 用 [JSON-RPC 2.0](https://www.jsonrpc.org/specification) 与项目 API 服务器进行通信。 TVM 用以下命令行来启动 API 服务器: + +``` bash +microtvm_api_server.py --read-fd --write-fd +``` + +命令通过 `--read-fd` 给出的文件描述符从 TVM 发送到服务器,然后 TVM 通过 `--write-fd` 给出的文件描述符从服务器接收回复。 + +## 在 Python 中实现 API 服务器的辅助函数 + +TVM 提供了辅助函数使得在 Python 中实现服务器更容易。要在 Python 中实现服务器,需创建 `microtvm_api_server.py`,并添加 `from tvm.micro.project_api import server`(或者,将这个文件复制到你的模板项目中——无需依赖——并将其导入)。接下来是子类 `ProjectAPIHander`: + +``` python +class Handler(server.ProjectAPIHandler): + def server_info_query(self, tvm_version): + # Implement server_info_query + # 实现 server_info_query + + def generate_project(self, model_library_format_path, standalone_crt_dir, project_dir, options): + # Implement generate_project + # 实现 generate_project + + # ... +``` + +最后,调用辅助函数 `main()`: + +``` python +if __name__ == "__main__": + server.main(Handler()) +``` + +## 使用来自 `tvmc` 的项目 API + +通过 `tvmc micro` 子命令,可以使用所有主要的项目 API 命令,这样简化了调试交互。调用 `tvmc micro --help` 获取更多信息。 \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/arch/arch/17-model_library_format.md b/versioned_docs/version-0.12.0/arch/arch/17-model_library_format.md new file mode 100644 index 00000000..4ba0b705 --- /dev/null +++ b/versioned_docs/version-0.12.0/arch/arch/17-model_library_format.md @@ -0,0 +1,98 @@ +--- +title: 模型库格式 +sidebar_position: 270 + +--- + +# 模型库格式 + +## 关于模型库格式 + +以前的 TVM 将生成的库导出为动态共享对象(例如 DLL(Windows)或 .so(linux))。通过使用 `libtvm_runtime.so` 将它们加载到可执行文件中,可以使用这些库执行推理。这个过程强依赖于传统操作系统提供的服务。 + +为了部署到非常规平台(例如那些缺乏传统操作系统的平台),TVM 提供了另一种输出格式——模型库格式(Model Library Format)。最开始的时候,这种格式主要应用于 microTVM。如果它可以用于其他用例(特别是,如果可以以模型库格式导出 BYOC 工件),那么就可以用作通用 TVM 导出格式。模型库格式是一个 tarball,其中包含每个 TVM 编译器输出的文件。 + +## 可以导出什么? + +编写代码时,仅能导出用 `tvm.relay.build` 构建的完整模型。 + +## 目录布局 + +模型库格式包含在 tarball 中。所有路径都是相对于 tarball 的根目录而言的: + +* `/` - tarball 的根目录 + * `codegen` - 所有生成的设备代码的根目录 + * (详见 [codegen](https://tvm.apache.org/docs/arch/model_library_format.html#codegen) 部分) +* `executor-config/` - 驱动模型推理的执行器配置 + * `graph/` - 包含 GraphExecutor 配置的根目录 + * `graph.json` - GraphExecutor JSON 配置 +* `metadata.json` - 此模型的机器可解析元数据 +* `parameters/` - 放置简化参数的根目录 + * `.params` - 模型 tvm.relay._save_params 格式的参数 +* `src/` - TVM 使用的所有源代码的根目录 + * `relay.txt` - 生成模型的 Relay 源代码 + +## 子目录说明 + +### `codegen` + +TVM 生成的所有代码都放在这个目录中。编写代码时,生成的模块树中的每个模块有 1 个文件,但此限制未来可能会更改。此目录中的文件应具有 `/(lib|src)/.` 形式的文件名。 + +这些组件描述如下: + +* `` - 标识运行代码的 TVM target。目前,仅支持 `host`。 +* `` - 标识此文件的唯一 slug。当前是 `lib`,其中 `` 是一个自动递增的整数。 +* `` - 标识文件名格式的后缀。目前是 `c` 或 `o`。 + +仅 CPU 模型的示例目录树如下所示: + +* `codegen/` - codegen 目录 + * `host/` - 为 `target_host` 生成的代码 + * `lib/` - 生成的二进制目标文件 + * `lib0.o` - LLVM 模块(如果使用 `llvm` target) + * `lib1.o` - LLVM CRT 元数据模块(如果使用 `llvm` target) + * `src/` - 生成的 C 源代码 + * `lib0.c` - C 模块(如果使用 `c` target) + * `lib1.c` - C CRT 元数据模块(如果使用 `c` target) + +### `executor-config` + +包含对执行器的机器可解析配置,这个执行器可以驱动模型推理。目前,只有 GraphExecutor(在 `graph/graph.json` 中)为此目录生成配置。读入这个文件,并将生成的字符串提供给 `GraphExecutor()` 构造函数进行解析。 + +### `parameters` + +包含机器可解析的参数。可提供多种格式,但目前只提供 `tvm.relay._save_params` 产生的格式。用 `tvm.relay.build` 构建时,`name` 参数是模型名称。在此目录 `.json` 中创建了一个文件。 + +### `src` + +包含 TVM 解析的源代码。目前,在 `src/relay.txt` 中只创建了 Relay 源代码。 + +## 元数据 + +机器可解析的元数据放在 tarball 根目录下的 `metadata.json` 文件中。元数据是具有以下键的字典: + +* `export_datetime`:模型库格式生成时的时间戳,为 [strftime](https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior) 格式 `"%Y-%M-%d %H:%M:%SZ"`。 +* `memory`:所有生成函数的内存使用总结。记录在 [内存使用总结](#memory-usage-summary) 中。 +* `model_name`:模型名称(例如,提供给 `tvm.relay.build` 的 `name` 参数)。 +* `executors`:模型支持的执行器列表。当前,此列表始终为 `["graph"]`。 +* `target`:将 `device_type`(潜在的整数,作为字符串)映射到描述用于该 `device_type` 的 Relay 后端的子 target 的字典。 +* `version`:标识模型库格式中格式的数字版本号。当元数据结构或磁盘结构更改时,这个数字会增加。本文档反映的是第 `5` 版。 + +### 内存使用总结 + +它是具有如下子键的字典: + +* `"main"`:`list[MainFunctionWorkspaceUsage]`。总结内存使用情况的列表,包括 main 函数使用的工作空间,以及调用的子函数。 +* `"operator_functions"`:`map[string, list[FunctionWorkspaceUsage]]`。将算子函数名称映射到一个列表,这个列表总结了函数使用的每个工作空间的内存使用情况。 + +`MainFunctionWorkspaceUsage` 字典具有以下键: + +* `"device"`:`int`。这个工作空间关联的 `device_type`。 +* `"workspace_size_bytes"`:`int`。工作空间中调用的函数和子函数所需的字节数。 +* `"constants_size_bytes"`:`int`。main 函数使用的常量的大小。 +* `"io_size_bytes"`:`int`。工作空间中函数和子函数使用的缓冲区大小的总和。 + +`FunctionWorkspaceUsage` 字典具有以下键: + +* `"device"`:`int`。这个工作空间关联的 `device_type`。 +* `"workspace_size_bytes"`:`int`。此函数在此工作空间中所需的字节数。 diff --git a/versioned_docs/version-0.12.0/arch/arch/runtimes/_category_.json b/versioned_docs/version-0.12.0/arch/arch/runtimes/_category_.json new file mode 100644 index 00000000..346153ae --- /dev/null +++ b/versioned_docs/version-0.12.0/arch/arch/runtimes/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 110 +} diff --git a/versioned_docs/version-0.12.0/arch/arch/runtimes/index.md b/versioned_docs/version-0.12.0/arch/arch/runtimes/index.md new file mode 100644 index 00000000..3994f1e8 --- /dev/null +++ b/versioned_docs/version-0.12.0/arch/arch/runtimes/index.md @@ -0,0 +1,214 @@ +--- +title: TVM Runtime 系统 +--- + +# TVM Runtime 系统 + +TVM 支持多种编程语言进行编译器堆栈开发和部署。本文档将介绍 TVM runtime 的关键元素。 + +![https://tvm.apache.org/images/release/tvm_flexible.png](https://tvm.apache.org/images/release/tvm_flexible.png) + +需要满足以下要求: + +* 部署:从 Python/JavaScript/C++ 语言调用编译好的函数。 +* 调试:在 Python 中定义一个函数,并从编译好的函数中调用它。 +* 链接:编写驱动程序代码以调用特定设备代码(CUDA),并从编译的主机函数中调用它。 +* 原型:从 Python 中定义一个 IR pass,并从 C++ 后端调用它。 +* 公开:将 C++ 开发的编译器堆栈用到前端(即 Python)。 +* 实验:将编译好的函数发送到嵌入式设备上直接运行。 + 我们期望能够用任何语言定义一个函数,然后用另一种语言调用。还期望将 runtime 内核最小化,部署到嵌入式设备。 + +## PackedFunc + +[PackedFunc](https://github.com/apache/tvm/blob/main/include/tvm/runtime/packed_func.h) 是一个简单而优雅的解决方案,它可以解决以上问题。单个 `PackedFunc` 对象代表一个函数调用,其调用者和被调用者可能使用不同的语言。 + +以下代码块提供了一个 C++ 示例: + +``` cpp +#include + +void MyAdd(TVMArgs args, TVMRetValue* rv) { + // 自动将参数转换为所需的类型。 + int a = args[0]; + int b = args[1]; + // 自动赋值返回给 rv + *rv = a + b; +} + +void CallPacked() { + PackedFunc myadd = PackedFunc(MyAdd); + // 返回 3 + int c = myadd(1, 2); +} +``` + +以上代码块中定义了一个 PackedFunc MyAdd。它有两个参数:`args` 代表输入参数,`rv` 代表返回值。该函数是类型擦除的,这意味着函数签名不限制传入或返回的输入类型。在后台调用 PackedFunc 时,它会将输入参数打包到堆栈上的 TVMArgs,并通过 TVMRetValue 获取结果。 + +由于 C++ 中的模板技巧,我们可以像调用普通函数一样来调用 PackedFunc。其类型擦除的性质,使得可以从动态语言(如 Python)中调用 PackedFunc,而无需为每个创建的新类型函数添加额外的胶水代码。以下示例在 C++ 中注册 PackedFunc,并在 Python 中调用。 + +``` cpp +// 在 C++ 中注册一个全局打包函数 +TVM_REGISTER_GLOBAL("myadd") +.set_body(MyAdd); +``` + +``` python +import tvm + +myadd = tvm.get_global_func("myadd") +# 打印 3 +print(myadd(1, 2)) +``` + +PackedFunc 的关键在于 `TVMArgs` 和 `TVMRetValue` 结构。我们限制了可传递的可能类型列表。以下是常见的类型: + +* 整数、浮点数和字符串 +* PackedFunc 本身 +* 编译模块的模块 +* DLTensor* 用于张量对象交换 +* TVM 对象表示 IR 中的任何对象 + +该限制使实现简单,且无需序列化。尽管是最小的,但 PackedFunc 对于深度学习部署的用例来说已经足够了,因为大多数函数只需要 DLTensor 或数字。 + +由于一个 PackedFunc 可以将另一个 PackedFunc 作为参数,因此可以将函数从 Python(作为 PackedFunc)传递给 C++。 + +``` cpp +TVM_REGISTER_GLOBAL("callhello") +.set_body([](TVMArgs args, TVMRetValue* rv) { + PackedFunc f = args[0]; + f("hello world"); +}); +``` + +``` python +import tvm + +def callback(msg): + print(msg) + +# 转换成 PackedFunc +f = tvm.convert(callback) +callhello = tvm.get_global_func("callhello") +# 打印 hello world +callhello(f) +``` + +TVM 提供了一个 [最小的 C API](https://github.com/apache/tvm/blob/main/include/tvm/runtime/c_runtime_api.h),因此可将 PackedFunc 嵌入到任何语言中。除了 Python,目前还支持 [java](https://github.com/apache/tvm/tree/main/jvm) 和 [javascript](https://github.com/apache/tvm/tree/main/web)。这种嵌入式 API 的原理很像 Lua,除了它用的是 C++ 语言而非新的语言。 + +PackedFunc 用于编译器和部署堆栈: + +* TVM 的所有编译器 pass 函数都以 PackedFunc 的类型暴露给前端 +* 编译好的模块还将编译好的函数作为 PackedFunc 类型返回 + +为了将 runtime 保持为最小,我们将 IR 对象支持与部署 runtime 隔离开来。生成的 runtime 大约需要 200K - 600K,具体取决于包含多少 runtime 驱动程序模块(例如,CUDA)。 + +与普通函数相比,调用 PackedFunc 的开销很小,因为它只在堆栈上保存了几个值,所以只要不包装小的函数即可。总之,PackedFunc 是 TVM 中的通用粘合剂,可以广泛使用它来支持编译器和部署。 + +## 模块 + +由于 TVM 支持多种类型的设备,因此需要支持不同类型的驱动程序。必须用驱动程序 API 来加载内核,以打包格式设置参数,并执行内核启动。 + +还需要为驱动程序 API 打补丁,以便公开的函数是线程安全的。因此,经常要在 C++ 中实现这些驱动粘合,并将它们提供给用户。但不能对每种类型的函数都这样做,PackedFunc 又可用来辅助实现。 + +TVM 将编译好的对象定义为 [Module](https://github.com/apache/tvm/blob/main/include/tvm/runtime/module.h)。用户可以从 Module 中获取编译好的函数为 PackedFunc。生成的编译代码可以从 runtime 中的 Module 中动态获取函数。它在第一次调用中缓存函数句柄,并在后续调用中再次使用。用它来将设备代码和回调函数链接到生成的代码中的任何 PackedFunc(例如 Python)。 + +ModuleNode 是一个抽象类,可由每种类型的设备实现。目前支持 CUDA、Metal、OpenCL 和加载动态共享库的模块。这种抽象使得新设备的引入变得容易,不需要为每种类型的设备重新生成主机代码。 + +## 远程部署 + +PackedFunc 和模块系统还可以轻松地将函数直接发送到远程设备。在底层有一个 RPCModule,用于序列化参数,从而进行数据移动并在远程启动计算。 + +![https://tvm.apache.org/images/release/tvm_rpc.png](https://tvm.apache.org/images/release/tvm_rpc.png) + +RPC 服务器本身是最小的,可以捆绑到 runtime 中。可以在 iPhone/android/raspberry pi 甚至浏览器上启动一个最小的 TVM RPC 服务器。服务器上的交叉编译和测试模块的交付可以在同一个脚本中完成。查看 [交叉编译和 RPC](/docs/tutorial/rpc) 以获取更多详细信息。 + +这种即时反馈带来了很多优势,例如,在 iPhone 上测试生成代码的正确性,不再需要从头开始在 swift/objective-c 中编写测试用例——可以使用 RPC 在 iPhone 上执行,将结果复制回来并在主机上通过 numpy 进行验证,也可以使用相同的脚本进行分析。 + +## TVM 对象和编译器堆栈 + +如前所述,在 PackedFunc runtime 系统之上构建编译器堆栈 API。因研究需要,编译器 API 在不断变化。要测试新的原语时,都需要一个新的语言对象或 IR 节点。但我们又不想经常更改 API。除此之外,还想 + +* 能够序列化任何语言对象和 IR +* 能够以前端语言探索、打印和操作 IR 对象以快速进行原型设计。 + +引入一个名为 [Object](https://github.com/apache/tvm/blob/main/include/tvm/runtime/object.h) 的基类来解决这个问题。编译器堆栈中的所有语言对象都是 `Object` 的子类,每个对象都包含一个字符串 type_key,用于唯一标识对象的类型。 + +之所以选择 string 而不是 int 作为类型键,是因为可以去中心化的方式来添加新的 `Object` 类,无需将代码添加回中心仓库。为了加快调度速度,在运行时为每个 type_key 分配一个整数 type_index。 + +一个 `Object` 通常可以在语言中的多个位置引用,因此使用 shared_ptr 来跟踪引用。用 `ObjectRef` 类表示对 `Object` 的引用。可以粗略地将 `ObjectRef` 类视为 `Object` 容器的 shared_ptr。还可以定义子类 `ObjectRef` 来保存 `Object` 的每个子类型。 `Object` 的每个子类都需要定义 VisitAttr 函数。 + +``` cpp +class AttrVisitor { +public: + virtual void Visit(const char* key, double* value) = 0; + virtual void Visit(const char* key, int64_t* value) = 0; + virtual void Visit(const char* key, uint64_t* value) = 0; + virtual void Visit(const char* key, int* value) = 0; + virtual void Visit(const char* key, bool* value) = 0; + virtual void Visit(const char* key, std::string* value) = 0; + virtual void Visit(const char* key, void** value) = 0; + virtual void Visit(const char* key, Type* value) = 0; + virtual void Visit(const char* key, ObjectRef* value) = 0; + // ... +}; + +class BaseAttrsNode : public Object { +public: + virtual void VisitAttrs(AttrVisitor* v) {} + // ... +}; +``` + +每个 `Object` 子类将覆盖它以访问其成员。以下是 TensorNode 的实现示例: + +``` cpp +class TensorNode : public Object { +public: + // 张量的形状 + Array shape; + // 张量内容中的简要数据类型 + Type dtype; + // 简述源码操作,可以是None + Operation op; + // 简述源操作的输出索引 + int value_index{0}; + // 简要构造函数 + TensorNode() {} + + void VisitAttrs(AttrVisitor* v) final { + v->Visit("shape", &shape); + v->Visit("dtype", &dtype); + v->Visit("op", &op); + v->Visit("value_index", &value_index); + } +}; +``` + +以上例子中,`Operation` 和 `Array` 都是 ObjectRef。 VisitAttrs 提供了一个反射 API 来访问对象的每个成员。可以用这个函数来访问节点,并递归地序列化任何语言对象。还可以用它在前端语言中轻松获取对象的成员。例如,以下代码中,访问了 TensorNode 的 op 字段: + +``` python +import tvm +from tvm import te + +x = te.placeholder((3,4), name="x") +# 访问 TensorNode 的 op 字段 +print(x.op.name) +``` + +可在不更改前端 runtime 的情况下,将新 `Object` 添加到 C++,从而轻松扩展编译器堆栈。 + +注意,这不是将成员提供给前端语言的最快方法,但可能是最简单的方法之一。并且它符合需求,因为我们主要用 Python 进行测试和原型设计,并仍用 C++ 来完成繁重的工作。 + +## 实现细节 + +PackedFunc 中的每个参数都包含一个关联值 [TVMValue](https://github.com/apache/tvm/blob/main/include/tvm/runtime/c_runtime_api.h#L135) 和一个类型代码。这种设计使得动态类型语言可直接转换为相应的类型,而静态类型语言在转换过程中,会进行 runtime 类型检查。 + +相关文件: + +* [packed_func.h](https://github.com/apache/tvm/blob/main/include/tvm/runtime/packed_func.h),用于 C++ API +* [c_runtime_api.cc](https://github.com/apache/tvm/blob/main/src/runtime/c_runtime_api.cc#L262),用于 C API 以及如何提供回调。 + 为了支持扩展类型,使用了注册表系统来注册类型相关信息,如 C++ 中对所有类型的支持,参阅 [扩展类型](https://github.com/apache/tvm/tree/main/apps/extension) 了解更多详细信息。 + +# Runtime 特定信息 + +* [Vulkan Runtime](vulkan) diff --git a/versioned_docs/version-0.12.0/arch/arch/runtimes/vulkan.md b/versioned_docs/version-0.12.0/arch/arch/runtimes/vulkan.md new file mode 100644 index 00000000..d396cb31 --- /dev/null +++ b/versioned_docs/version-0.12.0/arch/arch/runtimes/vulkan.md @@ -0,0 +1,61 @@ +--- +title: Vulkan Runtime +--- + +# Vulkan Runtime + +TVM 支持用 Vulkan 计算着色器(Vulkan compute shaders)来查询。所有计算内核都被编译成一个 SPIR-V 着色器,然后可以用 TVM 接口来调用它。 + +## Vulkan 的功能和限制 + +由于不同的 Vulkan 实现可能启用不同的可选功能,或具有不同的物理限制,因此代码生成必须知道哪些功能是可用的。这与 [Vulkan 功能表](#tvm-table-vulkan-capabilities) 中的特定 Vulkan 功能/限制一一对应。若未指定,TVM 会假定某个功能不可用,或者某个限制是 Vulkan 规范在 [必需限制](https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html#limits-minmax) 部分中保证的最小值。 + +这些参数应在定义 [Target](https://tvm.apache.org/docs/arch/device_target_interactions.html#tvm-target-specific-target) 时明确指定,或是可从设备中查询。若要从设备查询,可用特殊参数 `-from_device=N` 从 id 为 `N` 的设备中,查询所有 vulkan 设备参数。任何显式指定的附加参数,都会覆盖从设备查询的参数。 + +| **Target 参数** | **要求的 Vulkan 版本/扩**展 | **查询参数** | **默认值** | +|:---|:---|:---|:---| +| supported_subgroup_operations | Vulkan 1.1+ | VkPhysicalDeviceSubgroupProperties::supportedOperations | 0 (interpreted as [VkSubgroupFeatureFlagBits](https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VkSubgroupFeatureFlagBits.html)) | +| max_push_constants_size | | VkPhysicalDeviceLimits::maxPushConstantsSize | 128 bytes | +| max_uniform_buffer_range | | VkPhysicalDeviceLimits::maxUniformBufferRange | 16384 bytes | +| max_storage_buffer_range | | VkPhysicalDeviceLimits::maxStorageBufferRange | 227bytes | +| max_per_stage_descriptor_storage_buffer | | VkPhysicalDeviceLimits::maxPerStageDescriptorStorageBuffers | 4 | +| supports_storage_buffer_storage_class | VK_KHR_storage_buffer_storage_class | | false | +| supports_storage_buffer_8bit_access | VK_KHR_8bit_storage | VkPhysicalDevice8BitStorageFeaturesKHR::storageBuffer8BitAccess | false | +| supports_storage_buffer_16bit_access | VK_KHR_16bit_storage | VkPhysicalDevice16BitStorageFeaturesKHR::storageBuffer16BitAccess | false | +| supports_float16 | VK_KHR_shader_float16_int8 | VkPhysicalDeviceShaderFloat16Int8FeaturesKHR::shaderFloat16 | false | +| supports_float64 | | VkPhysicalDeviceFeatures::shaderFloat64 | false | +| supports_int8 | VK_KHR_shader_float16_int8 | VkPhysicalDeviceShaderFloat16Int8FeaturesKHR::shaderInt8 | false | +| supports_int16 | | VkPhysicalDeviceFeatures::shaderInt16 | false | +| supports_int64 | | VkPhysicalDeviceFeatures::shaderInt64 | false | + +截至 2021 年 5 月,还有一些 Vulkan 的实现没支持。例如,要支持 64 位整数。若不支持 Vulkan target,则会在 SPIR-V 代码生成期间报错。我们正努力消除这些限制,并支持其他 Vulkan 实现。 + +## SPIR-V 功能 + +某些特定于设备的功能还对应于 SPIR-V 功能或扩展,它们必须在着色器中声明,或对应于要使用某个功能所需的最低 SPIR-V 版本。TVM 生成的着色器将声明执行编译好的计算图所需的最小扩展/功能集,以及 SPIR-V 的最小允许版本。 + +若着色器生成需要 `Target` 中未启用的功能或扩展,则会引发异常。 + +| **Target 参数** | **要求的 SPIR-V 版本/扩展** | **声明的功能** | +|:---|:---|:---| +| supported_subgroup_operations | SPIR-V 1.3+ | Varies, see [VkSubgroupFeatureFlagBits](https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VkSubgroupFeatureFlagBits.html) | +| supports_storage_buffer_storage_class | SPV_KHR_storage_buffer_storage_class | | +| supports_storage_buffer_8bit_access | SPV_KHR_8bit_storage | StorageBuffer8BitAccess | +| supports_storage_buffer_16bit_access | SPV_KHR_16bit_storage | StorageBuffer16BitAccess | +| supports_float16 | | Float16 | +| supports_float64 | | Float64 | +| supports_int8 | | Int8 | +| supports_int16 | | Int16 | +| supports_int64 | | Int64 | + +## Vulkan 特定的环境变量 + +SPIR-V 代码生成和 Vulkan runtime 都有可以修改某些 runtime 行为的环境变量。这些变量用于调试,既可以轻松地测试特定代码路径,也可以根据需要输出更多信息。 + +若环境变量被设置为非零整数,则所有布尔标志都为真。未设置的变量、整数零或空字符串,都是错误的布尔标志。 + +* `TVM_VULKAN_DISABLE_PUSH_DESCRIPTOR` - 布尔标志。若为 True,TVM 将显式分配描述符,并且不会用 [VK_KHR_push_descriptor](https://khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VK_KHR_push_descriptor.html) 或 [VK_KHR_descriptor_update_template](https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VK_KHR_descriptor_update_template.html) 扩展。若为 False,TVM 将根据它们的可用性,来决定是否使用这些扩展。 +* `TVM_VULKAN_DISABLE_DEDICATED_ALLOCATION` - 布尔标志。若为 True,TVM 不会将内存分配标记为专用分配,并且不会使用 [VK_KHR_dedicated_allocation](https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VK_KHR_dedicated_allocation.html) 扩展。若为 False,TVM 将根据该缓冲区的 [VkMemoryDedicatedRequirements](https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VkMemoryDedicatedRequirements.html),来决定是否将内存分配标记为专用。 +* `TVM_VULKAN_ENABLE_VALIDATION_LAYERS` - 布尔标志。若为 True,TVM 将启用设备支持的 [Vulkan 验证层](https://github.com/KhronosGroup/Vulkan-LoaderAndValidationLayers/blob/master/layers/README.md)。若为 False,则不启用任何验证层。 +* `TVM_VULKAN_DISABLE_SHADER_VALIDATION` - 布尔标志。若为 True,则跳过使用 [spvValidate](https://github.com/KhronosGroup/SPIRV-Tools#validator) 完成的 SPIR-V 着色器验证。若为 False(默认),则 TVM 生成的所有 SPIR-V 着色器都使用 [spvValidate](https://github.com/KhronosGroup/SPIRV-Tools#validator) 进行验证。 +* `TVM_VULKAN_DEBUG_SHADER_SAVEPATH` - 目录的路径。若设置为非空字符串,Vulkan 代码生成器会将 tir、二进制 SPIR-V 和反汇编的 SPIR-V 着色器保存到此目录,用于调试。 diff --git a/versioned_docs/version-0.12.0/arch/index.md b/versioned_docs/version-0.12.0/arch/index.md new file mode 100644 index 00000000..7f2c921e --- /dev/null +++ b/versioned_docs/version-0.12.0/arch/index.md @@ -0,0 +1,274 @@ +--- +title: 设计与架构 +--- + +# 设计与架构 + +本文档适用于想要了解 TVM 架构和/或积极开发项目的开发者。本文档组织结构如下: + +* [编译流程示例](#example-compilation-flow) 概述了 TVM 将模型的高级描述转换为可部署模块所采取的步骤。 +* [逻辑架构组件](#logical-architecture-components) 部分描述了逻辑组件。后面的部分是针对每个逻辑组件的具体指南,按组件的名称编排。 +* [设备/ Target 交互](https://tvm.apache.org/docs/arch/device_target_interactions.html#tvm-target-specific-overview) 文档描述了 TVM 如何与所有受支持的物理设备,以及代码生成的 target 进行交互。 +* 查看 [开发者操作指南](/docs/dev/how_to) 获取实用开发技巧。 + +本指南提供了架构的一些补充视图。首先研究端到端的编译流程,并讨论关键的数据结构和转换。这种基于 runtime 的视图侧重于运行编译器时每个组件的交互。接下来研究代码库的逻辑模块及其关系。这部分提供了设计的静态总体视图。 + +## 编译流程示例 + +本指南研究编译器中的编译流程示例,下图显示了流程。在高层次,它包含以下步骤: + +* 导入:前端组件将模型引入到 IRModule 中,它包含了内部表示模型的函数集合。 +* 转换:编译器将 IRModule 转换为功能与之等效或近似等效(例如在量化的情况下)的 IRModule。许多转换与 target(后端)无关,并且允许 target 配置转换 pipeline。 +* Target 转换:编译器将 IRModule 转换(codegen)为指定 target 的可执行格式。target 的转换结果被封装为 *runtime.Module*,可以在 runtime 环境中导出、加载和执行。 +* Runtime 执行:用户加载 *runtime.Module*,并在支持的 runtime 环境中运行编译好的函数。 + +![/img/docs/tlc-pack/web-data/main/images/design/tvm_dyn_workflow.svg](/img/docs/tlc-pack/web-data/main/images/design/tvm_dyn_workflow.svg) + +### 关键数据结构 + +设计和理解复杂系统的最佳方法之一,就是识别关键数据结构和操作(转换)这些数据结构的 API。识别了关键数据结构后,就可以将系统分解为逻辑组件,这些逻辑组件定义了关键数据结构的集合,或是数据结构之间的转换。 + +**IRModule** 是整个堆栈中使用的主要数据结构。一个 IRModule(intermediate representation module)包含一组函数。目前支持两种主要的功能变体(variant): + +* **relay::Function** 是一种高级功能程序表示。一个 relay.Function 通常对应一个端到端的模型。可将 relay.Function 视为额外支持控制流、递归和复杂数据结构的计算图。 +* **tir::PrimFunc** 是一种底层程序表示,包含循环嵌套选择、多维加载/存储、线程和向量/张量指令的元素。通常用于表示算子程序,这个程序在模型中执行一个(可融合的)层。 在编译期间,Relay 函数可降级为多个 tir::PrimFunc 函数和一个调用这些 tir::PrimFunc 函数的顶层函数。 + +### 转换 + +前面介绍了关键数据结构,接下来讲转换。转换的目的有: + +* 优化:将程序转换为等效,甚至更优的版本。 +* 降级:将程序转换为更接近 target 的较低级别表示。 **relay/transform** 包含一组优化模型的 pass。优化包括常见的程序优化(例如常量折叠和死码消除),以及特定于张量计算的 pass(例如布局转换和 scale 因子折叠)。 + +在 Relay 优化流程的后期,运行 pass(FuseOps),将端到端函数(例如 MobileNet)分解为子功能(例如 conv2d-relu)段。这个过程帮助将原始问题分为两个子问题: + +* 所有子函数的编译和优化。 +* 整体执行结构:对生成的子函数进行一系列调用,执行整个模型。 使用下层 tir 阶段来编译和优化每个子函数。对于特定的 targets,也可以直接进入 target 转换阶段,使用外部代码生成器。 + +有几种不同的方法(在 relay/backend 目录)来处理对整体执行问题的调用。对于具有已知 shape 且没有控制流的简单模型,可以降级为图执行器,这个图执行器存储计算图中的执行结构。我们还支持用于动态执行的虚拟机后端。 + +最后,我们计划支持 ahead-of-time 编译,它将高级执行结构编译成可执行和生成的原始函数。所有这些执行模式都被统一的 **runtime.Module** 接口封装,指南的后半部分将进行讨论。 + +**tir/transform** 包含 TIR 级函数的转换过程。许多 tir passes 的目的是降级。例如,有些 pass 将多维访问展平为一维指针访问,将内联函数扩展至特定于 target 的函数,以及将函数入口修饰为满足 runtime 调用约定。当然,也有一些 pass 的目的是为了优化,例如访问索引简化和死码消除。 + +LLVM、CUDA C 和其他 target 编译器都可以在 target 阶段处理许多底层优化。因此,我们将底层优化(如寄存器分配)留给下游编译器,只关注它们未涵盖的优化。 + +### 搜索空间和基于学习的转换 + +到目前为止,我们介绍的转换 pass 都是确定且遵循一定规则的。 TVM 堆栈的设计目标之一是支持不同硬件平台的高性能代码优化。因此要研究尽可能多的优化选择,包括但不限于多维张量访问、循环分块行为、特殊加速器内存层次结构和线程。 + +定义一个要做出所有选择的启发式方法很难。因此,我们采用搜索和基于学习的方法。 + +首先定义一组用来转换程序的操作。示例操作包括循环转换、内联、向量化,这些操作称为**调度原语**。调度原语的集合定义了可用于程序的优化的搜索空间。 + +接下来,系统搜索不同的可能调度序列,选择最佳调度组合。搜索过程通常由机器学习算法指导。 + +搜索完成后,可以记录(可能融合的)算子的最佳调度顺序。然后编译器可以查找最佳调度序列,并将其应用于程序。注意,这个调度应用阶段与基于规则的转换**完全一样**,能够与传统 pass 共享相同的接口。 + +使用基于搜索的优化来处理初始 tir 函数生成问题。模块的这部分称为 AutoTVM(auto_scheduler)。随着 TVM 堆栈开发的深入,基于学习的转换将扩展到更多领域。 + +### Target 转换 + +target 转换阶段将 IRModule 转换为相应 target 的可执行格式。对于 x86 和 ARM 等后端,使用 LLVM IRBuilder 来构建内存中的 LLVM IR。还可以生成源代码级语言,例如 CUDA C 和 OpenCL。最后,支持通过外部代码生成器将 Relay 函数(子图)直接转换为特定 target 。 + +重要的是,最终代码生成阶段要尽可能轻量级。绝大多数的转换和降级要在 target 转换阶段之前进行。 + +我们还提供了一个 Target 结构来指定编译 target。target 转换阶段之前的转换也可能受到 target 的影响,例如,target 的向量长度会改变向量化行为。 + +### Runtime 执行 + +TVM runtime 的主要目标是提供一个最小的 API,从而能以选择的语言(包括 Python、C++、Rust、Go、Java 和 JavaScript)加载和执行编译好的工件。以下代码片段展示了一个 Python 示例: + +``` python +import tvm +# Python 中 runtime 执行程序示例,带有类型注释 +mod: tvm.runtime.Module = tvm.runtime.load_module("compiled_artifact.so") +arr: tvm.runtime.NDArray = tvm.nd.array([1, 2, 3], device=tvm.cuda(0)) +fun: tvm.runtime.PackedFunc = mod["addone"] +fun(a) +print(a.numpy()) +``` + +`tvm.runtime.Module` 封装了编译的结果。runtime.Module 包含一个 GetFunction 方法,用于按名称获取 PackedFuncs。 + +`tvm.runtime.PackedFunc` 是一种为各种构造函数消解类型的函数接口。runtime.PackedFunc 的参数和返回值的类型如下:POD 类型(int, float)、string、runtime.PackedFunc、runtime.Module、runtime.NDArray 和 runtime.Object 的其他子类。 + +`tvm.runtime.Module` 和 `tvm.runtime.PackedFunc` 是模块化 runtime 的强大机制。例如,要在 CUDA 上获取上述 *addone* 函数,可以用 LLVM 生成主机端代码来计算启动参数(例如线程组的大小),然后用 CUDA 驱动程序 API 支持的 CUDAModule 调用另一个 PackedFunc。OpenCL 内核也有相同的机制。 + +以上示例只处理了一个简单的 *addone* 函数。下面的代码片段给出了用相同接口执行端到端模型的示例: + +``` python +import tvm +# python 中 runtime 执行程序的示例,带有类型注释 +factory: tvm.runtime.Module = tvm.runtime.load_module("resnet18.so") +# 在 cuda(0) 上为 resnet18 创建一个有状态的图执行模块 +gmod: tvm.runtime.Module = factory["resnet18"](tvm.cuda(0)) +data: tvm.runtime.NDArray = get_input_data() +# 设置输入 +gmod["set_input"](0, data) +# 执行模型 +gmod["run"]() +# 得到输出 +result = gmod["get_output"](0).numpy() +``` + +主要的结论是 runtime.Module 和 runtime.PackedFunc 可以封装算子级别的程序(例如 addone),以及端到端模型。 + +### 总结与讨论 + +综上所述,编译流程中的关键数据结构有: + +* IRModule:包含 relay.Function 和 tir.PrimFunc +* runtime.Module:包含 runtime.PackedFunc + +编译基本是在进行关键数据结构之间的转换。 + +* relay/transform 和 tir/transform 是确定性的基于规则的转换 +* auto_scheduler 和 autotvm 包含基于搜索的转换 + +最后,编译流程示例只是 TVM 堆栈的一个典型用例。将这些关键数据结构和转换提供给 Python 和 C++ API。然后,就可以像使用 numpy 一样使用 TVM,只不过关注的数据结构从 numpy.ndarray 改为 tvm.IRModule。以下是一些用例的示例: + +* 用 Python API 直接构建 IRModule。 +* 编写一组自定义转换(例如自定义量化)。 +* 用 TVM 的 Python API 直接操作 IR。 + +## 逻辑架构组件 + +![/img/docs/tlc-pack/web-data/main/images/design/tvm_static_overview.svg](/img/docs/tlc-pack/web-data/main/images/design/tvm_static_overview.svg) + +*TVM Architecture Diagram*[¶](#logical-architecture-components) + +上图展示了项目中的主要逻辑组件。阅读以下部分,获取更多有关组件及其关系的信息。 + +## tvm/support + +support 模块包含基础架构最常用的程序,例如通用 arena 分配器(arena allocator)、套接字(socket)和日志(logging)。 + +## tvm/runtime + +runtime 是 TVM 堆栈的基础,它提供了加载和执行已编译工件的机制。runtime 定义了一组稳定的标准 C API,与 Python 和 Rust 等前端语言交互。 + +在 TVM runtime 中,*runtime::Object* 是除 *runtime::PackedFunc* 之外的主要数据结构之一。它是一个具有类型索引的引用计数基类,这个类型索引用来支持 runtime 类型检查和向下转换。 + +对象系统允许开发者向 runtime 引入新的数据结构,例如 Array、Map 和新的 IR 数据结构。 + +除了部署用例,编译器本身也大量使用 TVM 的 runtime 机制。所有 IR 数据结构都是 *runtime::Object* 的子类,因此可以从 Python 前端直接访问和操作它们。我们使用 PackedFunc 机制向前端公开各种 API。 + +runtime 子目录(例如 runtime/opencl)定义了 runtime 对不同硬件后端的支持。这些特定于硬件的 runtime 模块,为设备内存分配和设备功能序列化定义了 API。 + +*runtime/rpc* 实现了对 PackedFunc 的 RPC 支持,可以用 RPC 机制将交叉编译的库发送到远程设备,并对执行性能进行 benchmark 测试。rpc 基础架构支持从各种硬件后端收集数据,进行基于学习的优化。 + +* [TVM Runtime 系统](arch/runtimes) +* [特定 Runtime 信息](arch/runtimes#runtime-specific-information) +* [调试器](arch/debugger) +* [向 TVM 中添加虚拟机:Relay 虚拟机](arch/virtual_machine) +* [模块序列化简介](arch/introduction_to_module_serialization) +* [设备/Target 交互](arch/device_target_interactions) + +## tvm/node + +节点模块在 *runtime::Object* 之上,为 IR 数据结构添加了额外的功能。主要特征包括反射、序列化、结构等效和散列。 + +node 模块使得可以通过 Python 中的名称,直接访问 TVM IRNode 的任何字段。 + +``` python +x = tvm.tir.Var("x", "int32") +y = tvm.tir.Add(x, x) +# a 和 b 是 tir.Add 节点的字段 +# 可以直接使用字段名来访问 IR 结构 +assert y.a == x +``` + +还可以将任意 IR 节点序列化为 JSON 格式,然后将它们加载回来。保存/存储和检查 IR 节点的能力,为更容易访问编译器提供了基础。 + +## tvm/ir + +*tvm/ir* 文件夹包含了所有 IR 函数变体的统一数据结构和接口。 *tvm/ir* 中的组件由 *tvm/relay* 和 *tvm/tir* 共享,主要包括 + +* IRModule +* Type +* PassContext and Pass +* Op + +不同的函数变体(例如,relay.Function 和 tir.PrimFunc)可以在一个 IRModule 中共存。虽然这些变体的内容表示可能不同,但它们使用相同的数据结构来表示类型。 + +因此,可以用相同的数据结构来表示这些变体的函数(类型)签名。一旦明确定义了调用约定,统一的类型系统就可以用一个函数变体调用另一个函数。这为未来的跨函数变体的优化奠定了基础。 + +我们还提供了一个统一的 PassContext,用于配置 pass 行为,以及通用的复合 pass 来执行 pass pipeline。以下代码片段给出了 PassContext 配置的示例: + +``` python +# 配置 tir.UnrollLoop pass 的行为 +with tvm.transform.PassContext(config={"tir.UnrollLoop": { "auto_max_step": 10 }}): + # 受 pass 上下文影响的代码 +``` + +Op 是表示所有系统定义的原始算子/内联函数的通用类。开发者可以向系统注册新的 Ops,以及它们的额外属性(例如 Op 是否为元素)。 + +* [Pass Infrastructure](arch/pass_infra) + +## tvm/target + +target 模块包含将 IRModule 转换为 target runtime.Module 的所有代码生成器。它还提供了一个描述 target 的通用 Target 类。 + +通过查询 target 中的属性信息和注册到每个 target id(cuda、opencl)的内置信息,可以根据 target 定制编译 pipeline。 + +* [设备/Target 交互](arch/device_target_interactions) + +## tvm/tir + +TIR 包含低级程序表示的定义。用 *tir::PrimFunc* 来表示可通过 TIR pass 转换的函数。除了 IR 数据结构外,tir 模块还通过通用 Op 注册表,定义了一组内置内联函数及其属性,以及 *tir/transform* 中的转换 pass。 + +## tvm/arith + +该模块与 TIR 密切相关,低级代码生成的关键问题之一是分析索引的算术属性(arithmetic properties)——正性(positiveness)、变量界限和描述迭代器空间的整数集。arith 模块提供了一组进行(主要是整数)分析的工具。TIR pass 可以用这些分析来简化和优化代码。 + +## tvm/te + +te(tensor expression)代表「张量表达式」,这是一个特定领域的语言模块,它允许通过编写张量表达式来快速构造 *tir::PrimFunc* 变体(variant)。 + +重要的是,张量表达式本身并不是一个可以存储到 IRModule 中的自包含函数(self-contained function)。相反,它是 IR 的一个片段,可以拼接起来构建一个 IRModule。 + +*te/schedule* 提供了一组调度原语,来控制正在生成的函数。未来,可能会将其中一些调度组件引入到 *tir::PrimFunc* 本身。 + +* [InferBound Pass](arch/inferbound) +* [混合前端开发者指南](arch/hybrid_script) + +## tvm/topi + +虽然可以通过 TIR 或张量表达式(TE)为每个用例直接构造算子,但这样做很乏味。 *topi*(张量算子清单)提供了一组 numpy 定义的预定义算子(在 TE 或 TIR 中),并可在常见的深度学习任务中找到。还提供了一组常见的调度模板,来获得跨不同 target 平台的性能实现。 + +## tvm/relay + +Relay 是用于表示完整模型的高级功能 IR。*relay.transform* 中定义了各种优化。 Relay 编译器定义了多种方言,每种方言都旨在支持特定的优化风格。主要包括 QNN(用于导入预量化模型)、VM(用于降级到动态虚拟机)、内存(用于内存优化)。 + +* [Relay IR 简介](arch/relay_intro) +* [Relay 算子策略](arch/relay_op_strategy) +* [转换布局 Pass](arch/convert_layout) + +## tvm/autotvm + +AutoTVM 和 AutoScheduler 都是基于程序优化的自动搜索组件。它们在迅速发展中,主要包括: + +* cost 模型和特征提取。 +* 存储 cost 模型构建的程序 benchmark 结果的记录格式。 +* 一组程序转换的搜索策略。 + +自动化程序优化仍是热点研究领域。因此,我们采用了模块化的设计思路,以便研究人员通过 Python binding 来快速修改组件或应用自己的算法,也可以自定义搜索,以及从 Python binding 插入自己的算法。 + +* [benchmark 性能日志格式](arch/benchmark) + +## 前端 + +前端将来自不同框架的模型引入到 TVM 堆栈中。`tvm.relay.frontend` 是模型引入 API 的命名空间。 + +* [TensorFlow 前端](arch/tensorflow) + +## 安全 + +* [安全指南](arch/security) + +## microTVM + +* [microTVM 设计文档](arch/microtvm_design) +* [microTVM 项目 API](arch/microtvm_project_api) +* [模型库格式](arch/model_library_format) diff --git a/versioned_docs/version-0.12.0/conf.py b/versioned_docs/version-0.12.0/conf.py new file mode 100644 index 00000000..056ffb66 --- /dev/null +++ b/versioned_docs/version-0.12.0/conf.py @@ -0,0 +1,62 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'tvm-cn' +copyright = '2022, HyperAI' +author = 'HyperAI' + +# The full version, including alpha/beta/rc tags +release = '1.0.0' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'zh_CN' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/contribute/_category_.json b/versioned_docs/version-0.12.0/contribute/_category_.json new file mode 100644 index 00000000..e1d4231c --- /dev/null +++ b/versioned_docs/version-0.12.0/contribute/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 100 +} diff --git a/versioned_docs/version-0.12.0/contribute/ci.md b/versioned_docs/version-0.12.0/contribute/ci.md new file mode 100644 index 00000000..601f0136 --- /dev/null +++ b/versioned_docs/version-0.12.0/contribute/ci.md @@ -0,0 +1,103 @@ +--- +title: 使用 TVM 的 CI +sidebar_position: 8 +--- + +# 使用 TVM 的 CI + +TVM 用 Jenkins 在 [分支](https://ci.tlcpack.ai/job/tvm/)上运行 Linux 持续集成(CI)测试,并通过 [Jenkinsfile](https://github.com/apache/tvm/blob/main/Jenkinsfile) 中指定的构建配置 [pull request](https://ci.tlcpack.ai/job/tvm/view/change-requests/)。Windows 和 MacOS 的非关键任务在 GitHub Actions 中运行。 + +本页描述了贡献者和 committer 如何用 TVM 的 CI 来验证代码。可通过 [tlc-pack/ci](https://github.com/tlc-pack/ci) 仓库了解有关 TVM CI 设计的更多信息。 + +## 对 Contributor 而言 + +[Jenkins 的 BlueOcean 查看器](https://ci.tlcpack.ai/blue/organizations/jenkins/tvm/activity) 中的标准 CI 运行如下所示。 CI 运行通常需要几个小时才能完成,并且在 CI 完成之前无法 merge pull request(PR)。要诊断失败的步骤,请单击 failing pipeline stage,然后单击 failing step 来查看输出日志。 + +![The Jenkins UI for a CI run](https://github.com/tlc-pack/web-data/raw/main/images/contribute/ci.png) + +### 调试失败 + +当 CI 由于某种原因失败时,诊断问题的方法有如下几种。 + +#### Jenkins 日志 + +失败了首先按照失败作业上的红色 X 查看 CI 日志。注意: + +* Jenkins 默认不显示完整日志,在日志查看器的顶部有一个“Show complete log”按钮,点击可以查看纯文本日志。 +* `pytest` 失败总结在日志的底部,但可能需要向上滚动查看,才能知道失败的实际原因。 + +#### 重现失败 + +大多数 TVM Python 测试可以按照 [Testing](pull_request#pr-testing) 中的描述在 `pytest` 下运行。 + +### 提交 Issue + +[在 GitHub 上报告](https://github.com/apache/tvm/issues/new?assignees=&labels=&template=ci-problem.md&title=%5BCI+Problem%5D+) CI 的 issue,应该提供相关工作、commit 或 PR 的链接。 + +## 对 Maintainer 而言 + +本节讨论 TVM maintainer 的工作流程。 + +### 让 CI 保持正确的流程 + +本节讨论让 CI 正常运行的一般流程。 + +#### 同时合并导致 CI 失败 + +开发者在 merge 前依靠 TVM CI 来获取 PR 的信号。有时,两个不同的 PR 能分别通过 CI,但同时会破坏 `main`。反过来,又会导致错误显示在,基于失败 commit 的无关 PR 上。可从 [GitHub](https://github.com/apache/tvm/commits/main) 的 commit 状态图标或通过 [Jenkins](https://ci.tlcpack.ai/blue/organizations/jenkins/tvm/activity?branch=main) 来查找失败的 commit。 + +这些情况由 committer 负责,也鼓励其他人去帮助他们。这种情况的常见处理方式有: + +1. 回退有问题的 commit +2. 提交一个正向修复以解决问题 + +选择哪个选项取决于 committer 和 commit 作者。失败的 CI 会影响所有 TVM 开发者,所以应尽快修复。而当 PR 很大时,对其作者来说,回退尤其痛苦。 + +### 处理 Flakiness + +如果 PR 上的失败看起来与你的更改无关,并且没有看到任何有关失败的报告,你可以搜索 [与 flaky 测试相关的最新 GitHub Issue](https://github.com/apache/tvm/issues?q=is%3Aissue+%5BCI+%E9%97%AE%E9%A2%98%5D+Flaky+%3E) 和 [提交新 Issue](https://github.com/apache/tvm/issues/new?assignees=&labels=&template=ci-problem.md&title=%5BCI+Problem%5D+%3E) 来寻找解决方案。如果某个测试或者某类测试在多个 PR 或 main 上的 commit 引发了 flaky 失败,则应通过带有 [strict=False](https://docs.pytest.org/en/6.2.x/skipping.html#strict-parameter) 参数的 [pytest 的 @xfail 装饰器](https://docs.pytest.org/en/6.2.x/skipping.html#xfail-mark-test-functions-as-expected-to-fail) 和禁用 PR 链接的相关 issue 来禁用这个测试或者这类测试。 + +``` python +@pytest.mark.xfail(strict=False, reason="Flaky test: https://github.com/apache/tvm/issues/1234") +def test_something_flaky(): + pass +``` + +然后照常提交 PR: + +``` bash +git add +git commit -m'[skip ci][ci] Disable flaky test: ```` + +See # +' +gh pr create +``` + +### 跳过 CI + +对于回退和小的正向修复,将 `[skip ci]` 添加到回退的 PR 标题会让 CI 仅运行 lint。committer 应该注意,他们 merge 跳过 CI 的 PR,只是为了修复 `main` 上的失败 ,而非 submitter 想用 CI 更快 merge 更改。首次构建时会检查 PR 标题(尤其是在 lint 时,所以此后的更改不影响 CI,并且需要另一个 `git push` 重新触发该任务)。 + +``` bash +# 回退最近一次提交,确保在提交的开头插入 '[skip ci]' +git revert HEAD +git checkout -b my_fix +# 在你推送分支后,照常创建 PR +git push my_repo +# 示例:在已有 PR 的分支上跳过 CI +# 将这个提交添加到已有的分支上会导致新的 CI 跳过 Jenkins 运行 +git commit --allow-empty --message "[skip ci] Trigger skipped CI" +git push my_repo +``` + +### Docker 镜像 + +所有 CI 任务都在 Docker 容器(由 [docker/](https://github.com/apache/tvm/tree/main/docker) 文件夹中的文件构建)中运行其大部分的工作。这些文件通过 [docker-images-ci](https://ci.tlcpack.ai/job/docker-images-ci/) 任务每日在 Jenkins 中构建。这些容器的镜像在 [tlcpack Docker Hub](https://hub.docker.com/u/tlcpack) 中托管,并在 [Jenkinsfile.j2](https://github.com/apache/tvm/tree/main/Jenkinsfile.j2) 中引用。这些镜像可用标准的 Docker 命令在本地检查和运行。 + +### ci-docker-staging + +[ci-docker-staging](https://github.com/apache/tvm/tree/ci-docker-staging) 分支对 Docker 镜像的更新和 `Jenkinsfile` 的更改进行测试。当构建从 fork 的仓库得到的 PR 时,Jenkins 使用除了`Jenkinsfile` 本身(来自基本分支)之外的 PR 中的代码。由于构建分支时会使用分支中的 `Jenkinsfile`,所以具有写权限的 committer 必须将 PR 推送到 apache/tvm 中的分支,从而正确测试 `Jenkinsfile` 更改。如果 PR 修改了 `Jenkinsfile`,必须 @ [committer](https://github.com/apache/tvm/tree/main/CONTRIBUTORS.md),并要求他们把你的 PR 作为分支推送,从而测试更改。 + +### CI 监控轮换 + +有些测试也很不稳定,会因为与 PR 无关的原因而失败。 [CI 监控轮换](https://github.com/apache/tvm/wiki/CI-Monitoring-Runbook) 对这些故障进行监控,并在必要时禁用这些测试。编写测试的人负责修复好这些测试,并重新启用它们。 diff --git a/versioned_docs/version-0.12.0/contribute/code_guide.md b/versioned_docs/version-0.12.0/contribute/code_guide.md new file mode 100644 index 00000000..894ca591 --- /dev/null +++ b/versioned_docs/version-0.12.0/contribute/code_guide.md @@ -0,0 +1,87 @@ +--- +title: 代码指南及 Tips +sidebar_position: 6 +--- + +本文档为 reviewer 和贡献者汇总了 TVM 代码库中的技巧,大部分是在贡献过程中总结的经验教训。 + +## C++ 代码风格 + +- 使用 Google C/C++ 风格。 +- 面向公众的功能以 doxygen 格式记录。 +- 如果代码很短,使用具体类型声明而不是 `auto`。 +- 通过 const 引用传递(例如 `const Expr&`)而不是按值传递。除非函数通过拷贝构造函数或移动构造函数使用该值,在这种情况下,按值传递优于通过 const 引用传递。 +- 尽可能使用 `const` 成员函数。 + +我们可以用 `clang-format` 来规整代码风格。因为不同版本的 clang-format 可能会因版本而异,所以建议使用相同版本的 clang-format 作为主要版本。您还可以通过 docker 使用以下命令。 + +``` bash +# 通过 clang-format 运行指定文件 +docker/bash.sh ci_lint clang-format-10 [path-to-file] + +# 运行所有 linter,包括 clang-format +python tests/scripts/ci.py lint +``` + +clang-format 并不完美,必要时可以在某些代码区域上禁用 clang-format。 + +``` c++ +// clang-format off +void Test() { + // clang-format 将在这个区域禁用。 +} +// clang-format on +``` + +因为 clang-format 可能无法识别宏,所以建议像普通函数样式一样使用宏。 + +``` c++ +#define MACRO_IMPL { custom impl; } +#define MACRO_FUNC(x) + +// 不是首选,因为 clang-format 可能会将其识别为类型。 + +// 首选 +virtual void Func2() MACRO_IMPL; + +void Func3() { + // 首选 + MACRO_FUNC(xyz); +} +``` + +## Python 代码风格 + +- 函数和类以 [numpydoc](https://numpydoc.readthedocs.io/en/latest/) 格式记录。 +- 使用 `python tests/scripts/ci.py lint` 检查代码风格 +- 使用 `python 3.7` 中的语言特性 + +## 编写 Python 测试 + +用 [pytest](https://docs.pytest.org/en/stable/) 进行所有 Python 测试。 `tests/python` 包含所有测试。 + +如果您希望测试在各种 target 上运行,请使用 `tvm.testing.parametrize_targets()` 装饰器。例如: + +``` python +@tvm.testing.parametrize_targets +def test_mytest(target, dev): + ... +``` + +用 `target="llvm"`、`target="cuda"` 和其他几个运行 `test_mytest`。这可以确保测试由 CI 在正确的硬件上运行。如果只想针对几个 target 进行测试,请使用 `@tvm.testing.parametrize_targets("target_1", "target_2")`。如果想在单个 target 上进行测试,请使用来自 `tvm.testing()` 的相关装饰器。例如,CUDA 测试使用 `@tvm.testing.requires_cuda` 装饰器。 + +## 处理整型常量表达式 + +TVM 中经常需要处理整型常量表达式。在此之前,首先要考虑的是,是否真的有必要获取一个整型常量。如果符号表达式也有效并且逻辑行得通,那么尽可能用符号表达式。所以生成的代码也适用于未知的 shape。 + +注意,某些情况下,无法知道符号变量的符号这样的信息,这样的情况可做出一些假设。如果变量是常量,则添加精确的支持。 + +如果必须获取整型常量表达式,应该用 `int64_t` 类型而不是 `int` 来获取常量值,以避免整数溢出。通过 `make_const` 可以重构一个具有相应表达式类型的整数。相关示例如下所示: + +``` c++ +Expr CalculateExpr(Expr value) { + int64_t int_value = GetConstInt(value); + int_value = CalculateExprInInt64(int_value); + return make_const(value.type(), int_value); +} +``` diff --git a/versioned_docs/version-0.12.0/contribute/code_review.md b/versioned_docs/version-0.12.0/contribute/code_review.md new file mode 100644 index 00000000..7d1e2130 --- /dev/null +++ b/versioned_docs/version-0.12.0/contribute/code_review.md @@ -0,0 +1,105 @@ +--- +title: 代码 Reviews +sidebar_position: 3 +--- + +开源代码由具有不同背景、兴趣和目标的社区维护。因此,提供清晰、文档化和可维护的代码和流程非常重要。代码 review 是一个引导过程,用于共同发现潜在问题,提高代码质量,并就代码库及其假设对贡献者和 reviewer 进行教育。这也是确保多人共同维护相关代码的一种机制。鼓励贡献者在请求 review 之前将代码完善到可 review 的状态。这对于 committer 候选人来说尤其重要,因为 committer 不仅要参与写代码,还要参与 review。 + +本文档是开源代码 review 的在线指南。还请花一些时间阅读 [TVM 社区指南](community) 的一般开发过程。 + +## 建立信任 + +一个基于信任的社区,需要时间和精力来建立和维护。我们希望社区成员能够达成共识,并且以一种建设性的方式来协作。尽管我们都有着不同的背景、兴趣和目标,但我们必须共同努力寻找适合更大社区的解决方案。基于信任的协作不仅是 Apache 的关键租户,它对于发展社区,以及将社区成员提升为官方角色来说也很重要。 + +## 社区参与 + +欢迎大家对 PR 发表评论。我们鼓励 committer 在 merge 包含重大架构更改的 PR 之前等一段时间(例如三天),其目的是让人们有时间畅所欲言、充分讨论他们对 review 的兴趣并参与其中。 + +我们来自的背景不同,这对社区参与来说很重要。例如,一些社区成员工作在不同的时区,只在工作时间从事开源工作,其他时间可能在旅行或进行其他活动。在大型项目中工作有着集体认知是很重要的,这样的话没有人会成为瓶颈。虽然留出时间参与代码 review 很重要,但我们也不能阻止所有 reviewer 的所有更改。记住,帮助人们获得 PR 是鼓励更广泛参与的好方法,尤其是对于那些自愿贡献时间的人。 + +其中一部分是与其他 maintainer 的信任和沟通,如果将来要应用更改,PR 的作者要兑现他们的承诺。committer 有义务听取任何来自 PMC 成员或新贡献者的反馈,并考虑要采取什么行动。 + +## 仔细阅读代码 + +有时我们会快速通读代码,只关注代码的某一部分,社区应该欢迎这些类型的评论。不过,它们只是代码 review 的一部分,也应成为更全面的反馈的一部分。要仔细地进行代码 review 会耗费大量时间,这个过程花费的时间可能比实际编写代码的时间还要长。 + +例如,就算只是收到关于 PR 次要方面的高度批评性反馈,都会让人觉得投入的时间和精力在 review 期间没有得到回报,这会让他们非常沮丧。不管是作为贡献者还是 committer 时,同理心都很重要,它可以帮助你成为更有效的代码 reviewer 和贡献者。 + +我们希望所有 committer 在签名(sign off)之前仔细阅读并理解代码。当 committer 点击 merge 按钮时,牵涉到很多人对他的信任。同时,我们承认有时会出现问题,在这种情况下,merger 有责任确保采取正确的后续行动。 + +## 尊重他人 + +- 所有发表评论的人:提出建设性评论不仅有助于新贡献者及时获得 PR,还有助于提高新成员的参与度。 +- 写给作者:reviewer 会花大量时间阅读代码,仔细 review 可能与从头开始编写代码一样耗费时间。恭敬地处理 review 意见,并在将来 review 其他人的代码,来回报他们的 review。 + +最重要的是专注于进行建设性的对话,并在作为 reviewer 与其他人互动时尽量把他人往好处想。如果流程中有些东西不起作用,可以考虑与其他贡献者面对面交流,并讨论如何改进流程或沟通。 + +## 代码质量需要考虑的因素 + +高质量代码对于项目的长期发展至关重要。在代码 review 期间,需要从多方面对代码质量进行评估: + +- F0:整体架构。这包括公共模块、关键数据结构和公共接口的定义。从长远来看,良好的架构选择对于项目的成功至关重要。 +- F1:架构一致性。通常有多种方法可以实现新功能。我们必须确保新功能与之前的整体架构选择保持一致,并能与现有代码进行良好的交互。每个新功能都会增加项目的复杂度,而一致的设计可以最大限度地降低新功能的复杂度,从长远来看利于代码维护。 +- F2:代码稳健性和测试覆盖。确保代码在所有可能的场景(平台)中正确运行,确保新功能的测试覆盖。为遇到报错的用户清除错误消息。 +- F3:面向用户的 API 文档:请务必编写面向用户的公共 API 和关键模块接口的文档。这包括 API、出现在公共接口中的数据结构(即 *include/tvm* 和面向用户的 python API)。我们鼓励代码对应有完善的文档(其中包含对内部 API 的描述,这些内部 API 在多个地方被使用),另请参阅 F4。 +- F4:代码可读性。可读性涉及多个方面:有指导意义且一致的函数名称、整体流程的清晰实现、复杂代码逻辑和内部函数的描述性注释。可读性高的代码更容易维护。 + +架构设计和一致性至关重要,因为它们可能会引起技术问题的长期累积。因此,committer 在合并代码之前应该仔细地考虑这些因素。 + +代码贡献需要包含测试覆盖和 API 文档。 + +与其他问题相比,代码可读性是一个相对主观的问题。不同的人对如何写出最好的代码有不同的看法。reviewer 应提出一些有建设性和可操作的意见。同时,代码 review 不应该成为,一种让其他人完全按照 reviewer 的方式来写代码的方式。相反,还要考虑到一点,就是你认为很容易理解或可以接受的东西,可能不适用于更大的社区或其他成员。请根据贡献的内容和范围,以及贡献者的来源来判断什么是合适的。 + +我们写代码要遵循通用的 [代码指南和提示](code_guide)。风格指南可以使得,即便在原作者离开很久之后,其他人仍可以阅读和维护代码。风格指南不仅仅关乎代码的格式化,还记录——代码、命名变量,以及没有被自动格式化程序强制执行的约定——的正确方式。 + +## 共识构建 + +在代码 review 期间可能会发生分歧。我们鼓励在相关人员之间建立共识,同时我们也致力于在 OSS 中彼此建立信任。 OSS 的性质意味着,我们为了取得稳步进展,有时会在不太重要的问题上做出妥协,同时欢迎更广泛的社区参与。但是妥协意味着有些事情不完全如我们所想,对社区领袖来说也是如此。 + +- 在建设性的基于技术的对话中保持文明并建立共识。 +- 领域所属的 committer 可以充当主持人,通过考虑所有对话来推动讨论,并提出推进的解决方案。 +- 因为赋予了 committer(主持人)很大的信任,所以他们应该在签署之前仔细阅读 PR。此外,如果合并引发了问题,那么合并者也应承担后续的责任。 + +## 一致性 + +最后一点,我们都是人,很难始终保持一致。如果贡献者认为某些地方你没有遵循一致性方针,请根据具体情况进行相应调整。随着社区的发展,流程和指导方针将会不断地迭代。我们的目标是努力保持一致和客观。 + +## 其他建议 + +### 仔细考虑 API 和数据结构 + +最小且稳定的 API 对项目而言至关重要。好的 API 会产生很大影响。始终要仔细考虑所有方面,包括命名、参数定义和行为。 + +如果可能,在代码 review 过程中仍要多关注已经提出的 API 设计。请记住,改进代码实现是很容易的一件事情,但是,一旦 API 被接受就很难更改变了。我们应该以相同的方式处理贯穿多个模块共享的数据结构(例如 AST)。如果不确定的话,请在 commit 之前与其他开发人员充分交流探讨。 + +以下是一些对于设计 API 而言,非常实用的原则: + +- 如果功能重叠,请与现有知名包的 API 保持一致。例如,张量算子 API 应始终与 numpy API 保持一致。 +- 与同一项目中的现有 API 保持一致。例如,我们应该在整个优化过程中使用相同的参数排序,这样在使用它们时才不会有发生“意外”。 +- 想想未来 API 会不会发生变化。例如,随着在构建中添加更多的优化,我们会有更多选项,例如 loop_unrolling 和设备放置策略。我们可将优化 knob 打包到构建配置对象中,通过这种方式构建的 API, 即使在未来可能会被丰富,但长久来看也是稳定的。 +- 写文档。文档对于 API 来说是强制性的,有时候写文档可以帮助我们进一步思考设计,以及是否需要添加进一步的说明。 +- 最小化。想想用户为了使用 API 要写多少行代码。尽可能删除抽象层。 + +### 最小化依赖 + +引入依赖时始终要保持谨慎。虽然代码复用和避免重新造轮子很重要,但依赖关系会增加用户在部署中的负担。一个好的设计原则是,对于某个特性或功能,应该只有在用户实际使用它时,才具有依赖关系。 + +### 简洁的实现 + +这里的基本原则是:多用向量化数组代码而不是循环;使用现有 API 解决问题。 + +### 记录代码 review 中的经验 + +对于一些常见或反复出现的问题,请将其添加到 [代码指南和提示](code_guide) 中。请求更改时可参考指南文档,这有助于将经验分享到所有社区。 + +### 从其他代码 review 中学习 + +可以有多个 reviewer review 相同的更改。很多时候,其他 reviewer 可能会发现你没有发现的问题。尝试从其他代码 review 中学习并积累经验。 + +### 明确同意和请求更改 + +贡献者和代码所有者可以向多个 reviewer 请求代码 review。当你的评论在代码 review 中变为已解决时,记得同意更改:单击 pull request 中的 changes 选项卡,然后选择 approve;或者评论代码,然后单击 request changes。如果某些 reviewer 没有及时回复(例如一周)并且现有 review 足够,代码所有者可以决定是否 merge 代码。 + +### Reviewers + +reviewer 应及时对请求 review 的 pull request 给出反馈。review 代码是项目健康的重要组成部分,应被视为贡献者的常规责任。自动化工具可用来改善这个问题:在一段时间内没有任何活动的 PR,会收到一条机器人评论提示相关责任方。 diff --git a/versioned_docs/version-0.12.0/contribute/committer_guide.md b/versioned_docs/version-0.12.0/contribute/committer_guide.md new file mode 100644 index 00000000..0dc19d75 --- /dev/null +++ b/versioned_docs/version-0.12.0/contribute/committer_guide.md @@ -0,0 +1,47 @@ +--- +title: Committer 手册 +sidebar_position: 4 +--- + +本文档长期更新中,旨在为 committer 提供一些实用技巧。其中大部分是在开发过程中总结的经验教训。我们欢迎每一位 committer 为本文档做出贡献。有关提交和一般开发过程的概述,请参阅 [TVM 社区指南](community)。 + +## 社区优先 + +社区成员努力推动了项目的发展,使得这个项目看起来相当完美。当我们在做决定的时候,要时刻把社区放在第一位。以下是我们可以提出的一些示例问题: + +- 我该如何鼓励新的贡献者更多地参与项目? +- 我可以帮其他 committer 节省时间吗? +- 我是否让社区的其他人参与设计提案? + +## 公共档案原则 + +尽管类似面对面讨论这样的私人渠道有益于发展,但它们也限制了更广的社区参与。 Apache 的开发方式要求所有决策都要在公共渠道中实施,因为这些渠道被归档并可供所有人访问。因此,任何贡献者都可以通过浏览档案来跟上开发的步伐,并随时加入开发。 + +这一原则适用于所有贡献者,对 committer 来说尤其重要。下面是该原则的一些示例应用: + +- 当从个人渠道获得与项目相关的问题时,鼓励新开一个论坛来讨论,这样的话社区中的其他人也可以受益。 +- 在面对面讨论之后,将摘要发送到公共渠道(例如 RFC 或线程讨论)。 + +## 认领 pull request + +下面是一些认领 pull request 的技巧。也可以查看 [代码 Reviews](code_review)。 + +- 将 PR 分配给自己,以便其他 committer 知道 PR 已被处理。 +- 使用状态标签来标注当前状态。 +- 检查是否需要发送 RFC。 +- 如果贡献者没有请求 reviewer 去 review 自己的代码,请提示一下他。如果 PR 来自一个新的贡献者,提示他去请求 reviewer,并让他下次也这样做。 +- 审核 review,要求 reviewer 明确批准。 +- 将 PR 标记为已接受状态,并感谢贡献者/reviewer。 +- merge PR + +## 时间管理 + +committer 可以做很多事情,例如主持讨论、review pull request 和贡献代码。 + +在开源项目上工作会收获很多,但有时也会有点不知所措。一点时间管理可能有助于缓解这个问题。例如,一些 committer 一周内有一天是“社区日”,他们积极管理优秀的 PR,但在其余时间较少关注社区。 + +记住,只要能提供有价值的贡献就是有意义的,因此在为项目做贡献时可以慢慢来。 + +## 广泛合作 + +我们倾向于只与认识的人互动。然而,广泛的合作对于项目的成功是必要的。鼓励多为社区中不认识的人认领 PR,或请求他们 review 代码。 \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/contribute/community.md b/versioned_docs/version-0.12.0/contribute/community.md new file mode 100644 index 00000000..ed3450c8 --- /dev/null +++ b/versioned_docs/version-0.12.0/contribute/community.md @@ -0,0 +1,28 @@ +--- +title: TVM 社区指南 +sidebar_position: 1 +--- + +TVM 采用 Apache 风格的模型,以优胜劣。在我们看来,创造一个包容性的社区(人人都可以使用,做贡献,也可以影响项目的走向)非常重要。有关当前贡献者列表,请参见 [CONTRIBUTORS.md](https://github.com/apache/tvm/blob/main/CONTRIBUTORS.md)。 + +## 一般开发流程 + +欢迎社区中的每个人发送补丁、文档以及为项目提出新方向。这里的关键指导方针是让社区中的每个人都能参与决策和发展。当提出重大更改时,应发送 RFC 以供社区讨论。我们鼓励利用诸如 issue,论坛和邮件的可归档渠道来公开讨论,以便社区中的每个人都可以参与并在以后回顾该过程。 + +代码 review 是保证代码质量的关键方法之一。高质量的代码 review 可以防止长期的技术问题累积,对项目的成功至关重要。pull request 在 review 完后才能 merge。具有相应领域专业知识的 committer 审查完 pull request 再 merge。相应的可以请求多个熟悉代码领域的 reviewer。我们鼓励贡献者自己请求代码 review 并帮助 review 彼此的代码——记住每个人都自愿为社区贡献自己的时间,高质量的代码 review 本身将花费与实际的代码贡献一样多的时间,如果这样做,你的代码也会快速得到 review。 + +社区应努力通过讨论就技术决策达成共识。我们希望 committer 和 PMC 以外交方式主持技术讨论,并在必要时提供具有明确技术推理的建议。 + +## Committers + +committer 是被授予项目写入权限的人。committer 还需要监督他们负责的代码的 review 过程。贡献领域可以采取各种形式,包括代码贡献和代码 review、文档、宣传和对外合作。在一个高质量和健康的项目中,committer 的作用至关重要。社区积极从贡献者中寻找新的 committer。以下是帮助社区识别潜在 committer 的有用特征的列表: + +- 对项目的持续贡献,包括对 RFC 的讨论、代码 review 和新特性的提出以及其他开发活动。熟悉并能够掌握项目的一个或多个领域。 +- 贡献质量:高质量、可读性高的代码贡献表现为:无需大量代码 review,便可 merge pull request;有创建整洁、可维护的代码并包含良好的测试用例的历史;提供有用信息的代码 review,从而帮助其他贡献者更好地遵守标准。 +- 社区参与:积极参与论坛交流,通过教程、讲座和对外合作来推广项目。我们鼓励 committer 广泛合作,例如与社区成员线上一起 review 代码和讨论项目设计。 + +[项目管理委员会(PMC)](https://projects.apache.org/committee.html?tvm) 由活跃的 committer 组成,负责主持讨论、管理项目发布并提名新的 committer/PMC 成员。候选人通常由 PMC 内部讨论提出,然后共识批准(即至少 3+1 票,并且没有否决票)。任何否决票都必须附有理由。PMC 应该通过维护社区实践和准则,把 TVM 打造成一个更好的社区。PMC 应尽可能提名自己组织之外的新候选人。 + +## Reviewers + +reviewer 是积极为项目做出贡献并愿意参与新贡献的代码 review 的人,他们源自活跃的贡献者。committer 应明确征求 reviewer 的 review 意见。高质量的代码 review 可以防止长期的技术问题累积,这对项目的成功至关重要。项目的 pull request 必须由至少一名 reviewer review 才能 merge。 \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/contribute/document.md b/versioned_docs/version-0.12.0/contribute/document.md new file mode 100644 index 00000000..52d0765d --- /dev/null +++ b/versioned_docs/version-0.12.0/contribute/document.md @@ -0,0 +1,133 @@ +--- +title: 文档指南 +sidebar_position: 5 +--- + +TVM 文档大致遵循 [Divio 技术文档写作指南](https://documentation.divio.com)。之所以选择该指南,是因为它是一个“简单、全面且几乎普遍适用的方案,且已在广泛的领域和应用中证明”。 + +本文档描述了 TVM 文档的组织结构,以及如何撰写新文档。参阅 [docs/README.md](https://github.com/apache/tvm/tree/main/docs#build-locally) 来了解如何构建文档。 + +## 四种文档类型 + +### 入门教程 + +这是引导新用户加入项目的分步指南。入门教程只需要让用户学会使用软件即可,无需让他们知道底层原理。底层原理将在其他类型的文档中讲解。入门教程侧重于成功的初体验,它是将新手转变为新用户和开发人员的最重要文档。 + +完整的端到端教程——从安装 TVM 和支持 ML 软件,到创建和训练模型,再到编译到不同的架构——将让新用户以尽可能最有效的方式来使用 TVM。它让初学者知道那些必备知识,这与操作指南形成鲜明对比,操作指南旨在回答有一定经验的用户会提出的问题。 + +教程必须是可复现且可靠的,因为不成功的体验会让用户寻找其他解决方案。 + +### 操作指南 + +这是有关如何解决特定问题的分步指南。用户可以提出有意义的问题,然后文档给出答案。这类文档举例来说大概是这样的:“如何为 ARM 架构编译一个优化模型?”或“如何编译和优化 TensorFlow 模型?”这些文档应该足够开放,以便用户可以看到如何将其应用于新的用例。实用性比完整性更重要。标题应该能让用户看出这个操作指南解决的是什么问题。 + +教程与操作指南有何不同?教程面向刚接触 TVM 的开发者,重点是成功地将他们引导到软件和社区之中。而操作指南则侧重于,在对它有基本理解的前提下,完成特定任务。教程假定用户没有任何先备知识,帮助他快速上手。操作指南则假设用户具备最基本的知识,旨在指导他完成特定任务。 + +### 参考 + +参考文档描述了软件的配置和操作方式。 API、关键函数、命令和接口都是参考文档的候选对象。它是让用户构建自己的界面和程序的技术手册。它以信息为导向,注重列表和描述。可以假设读者已经掌握了软件的工作原理,并且正在寻找特定问题的特定答案。理想情况下,参考文档应该和代码库具有相同的结构,并且尽可能是自动生成的。 + +### 架构指南 + +架构指南是关于某个主题的解释和背景材料。这些文档帮助用户理解应用环境。为什么事情是这样的?设计决策是什么,考虑了哪些替代方案,描述现有系统的 RFC 是什么?它包括了学术论文和软件相关出版物的链接。在这些文档中,你可以探索到矛盾点在哪里,并帮助读者理解软件是如何、以及为什么按照现在的方式构建的。 + +它既不是操作指南,也不是如何完成任务的描述。相反,这些文档聚焦能够帮助用户理解项目的更高级的概念。通常,这些是由项目的架构师和开发人员编写的,有助于帮助用户和开发人员更深入地了解为什么软件是这样工作的,以及如何以与底层设计原则一致的方式,来对它做出贡献。 + +### TVM 的特殊考量 + +TVM 社区有一些特殊考量,偏离了 Divio 的简单文档风格的原则。第一点就是用户和开发者社区之间经常存在重叠。很多项目用不同的系统记录开发者和用户的体验,但 TVM 将这两者放在一起,只在合适的时候做一些区分。因此,教程和操作指南将分为关注用户体验的“用户手册”和关注开发者体验的“开发手册”。 + +下一个考量是 TVM 社区中存在一些特殊主题,值得投入较多的关注。这些主题包括但不限于 microTVM 和 VTA。可以创建特殊的“主题指南”来索引现有材料,并提供相关超链接。 + +为方便新手,将制作一个特殊的“入门”部分,其中包含安装说明、为什么使用 TVM 以及其他初体验文档。 + +## 技术细节 + +我们将 [Sphinx](http://sphinx-doc.org) 作为主要文档。 Sphinx 支持 reStructuredText 和 markdown。我们鼓励使用功能更丰富的 reStructuredText。注意,Python 文档字符串和教程允许嵌入 reStructuredText 语法。 + +构建文档的指导请参阅 [docs/README.md](https://github.com/apache/tvm/tree/main/docs#build-locally)。 + +### Python 参考文档 + +使用 [numpydoc](https://numpydoc.readthedocs.io/en/latest/) 格式来记录函数和类。以下代码段提供了一个文档字符串示例。记录所有公共函数,必要时提供支持功能的使用示例(如下所示)。 + +``` python +def myfunction(arg1, arg2, arg3=3): + """简单描述我的函数。 + + Parameters + ---------- + arg1 : Type1 + arg1 的描述 + + arg2 : Type2 + arg2 的描述 + + arg3 : Type3, 可选 + arg3 的描述 + + Returns + ------- + rv1 : RType1 + 返回类型 1 的描述 + + Examples + -------- + .. code:: python + + # myfunction 的使用示例 + x = myfunction(1, 2) + """ + return rv1 +``` + +注意在文档的各个部分之间留空行。在上例中,`Parameters`、`Returns` 和 `Examples` 前面必须有一个空行,以便正确构建文档。要往文档里添加新功能时,需要将 [sphinx.autodoc](http://www.sphinx-doc.org/en/master/ext/autodoc.html) 规则添加到 [docs/reference/api/python](https://github.com/apache/tvm/tree/main/docs/reference/api/python) 中)。可以参考该文件夹下的已有的文件来了解如何添加功能。 + +### C++ 参考文档 + +使用 Doxygen 格式来记录 C++ 函数。以下片段展示了一个 C++ 文档字符串的示例。 + +``` c++ +/*! + * \brief 我的函数的简单描述 + * \param arg1:arg1 的描述 + * \param arg2:arg2 的描述 + * \returns 描述返回值 + */ +int myfunction(int arg1, int arg2) { + // 必要的时候添加一些注释来阐明内部逻辑 +} +``` + +除了记录函数的用法外,我们还强烈建议贡献者添加有关代码逻辑的注释以提高可读性。 + +### Sphinx Gallery 操作指南 + +我们用 [sphinx-gallery](https://sphinx-gallery.github.io/) 构建了很多 Python 操作指南。可以在 [gallery](https://github.com/apache/tvm/tree/main/gallery) 下找到源代码。注意:注释块是用 reStructuredText 而不是 markdown 编写的,请注意语法。 + +操作指南代码将在我们的构建服务器上运行以生成文档页面。所以我们可能会有一个限制,比如无法访问远程 Raspberry Pi,在这种情况下,向教程中添加一个标志变量(如 `use_rasp`),并允许用户通过更改一个标志轻松切换到真实设备。然后用已有的环境来演示使用。 + +如果为操作指南添加一个新的分类,则需要添加对 [conf.py](https://github.com/apache/tvm/tree/main/docs/conf.py) 和 [how-to index](https://github.com/apache/tvm/tree/main/docs/how-to/index.rst) 的引用。 + +### 引用文档中的另一个位置 + +请使用 sphinx 的 `:ref:` 标记来引用同一文档中的另一个位置。 + +``` rst +.. _document-my-section-tag + +My Section +---------- + +可以使用 :ref:`document-my-section-tag` 来引用 My Section。 +``` + +### 带有图像/图形的文档 + +reStructuredText 的 [figure](https://docutils.sourceforge.io/docs/ref/rst/directives.html#figure) 和 [image](https://docutils.sourceforge.io/docs/ref/rst/directives.html#image) 元素允许文档包含图像 URL。 + +为 TVM 文档创建的图像文件应该在 [https://github.com/tlc-pack/web-data](https://github.com/tlc-pack/web-data) 仓库中,而使用这些图像的 *.rst* 文件应该在 TVM 仓库([https://github.com/apache/tvm](https://github.com/apache/tvm))。 + +这需要两个 GitHub Pull Request,一个用于图像文件,另一个用于 *.rst* 文件。contributor 与 reviewer 之间可能需要讨论来协调 review 的过程。 + +重要提示:使用上述的两个 pull request 时,请先在 [https://github.com/tlc-pack/web-data](https://github.com/tlc-pack/web-data) 中 merge pull request,然后再在 [https://github.com/apache/tvm](https://github.com/apache/tvm) 中 merge pull request。这有助于确保 TVM 在线文档中的所有 URL 链接都是有效的。 diff --git a/versioned_docs/version-0.12.0/contribute/error_handling.md b/versioned_docs/version-0.12.0/contribute/error_handling.md new file mode 100644 index 00000000..98db3975 --- /dev/null +++ b/versioned_docs/version-0.12.0/contribute/error_handling.md @@ -0,0 +1,84 @@ +--- +title: 处理报错 +sidebar_position: 10 +--- + +TVM 包含结构化的错误类以表示特定类型的错误。请尽可能提出特定的错误类型,以便用户可以在必要时写代码来处理特定的错误类别。可以直接在 Python 中抛出特定的错误对象。在 C++ 等其他语言中,只需给错误消息添加 `:` 前缀(见下文)。 + +:::note +有关报错列表,请参考 [tvm.error](https://tvm.apache.org/docs/reference/api/python/error.html#module-tvm.error)。 +::: + +## 在 C++ 中抛出特定错误 + +可以给错误消息添加 `:` 前缀来抛出相应类型的错误。注意,当消息中没有错误类型前缀时,不必添加新类型,默认会抛出 [tvm.error.TVMError](https://tvm.apache.org/docs/reference/api/python/error.html#tvm.error.TVMError) 错误。此机制适用于 `LOG(FATAL)` 和 `ICHECK` 宏。具体示例见以下代码: + +``` c +// src/api_test.cc +void ErrorTest(int x, int y) { + ICHECK_EQ(x, y) << "ValueError: expect x and y to be equal." + if (x == 1) { + LOG(FATAL) << "InternalError: cannot reach here"; + } +} +``` + +上述函数作为 PackedFunc 注册到 Python 前端,名称为 `tvm._api_internal._ErrorTest`。如果我们调用注册函数会发生以下情况: + +``` +>>> import tvm +>>> tvm.testing.ErrorTest(0, 1) +Traceback (most recent call last): + File "", line 1, in + File "/path/to/tvm/python/tvm/_ffi/_ctypes/function.py", line 190, in __call__ + raise get_last_ffi_error() +ValueError: Traceback (most recent call last): + [bt] (3) /path/to/tvm/build/libtvm.so(TVMFuncCall+0x48) [0x7fab500b8ca8] + [bt] (2) /path/to/tvm/build/libtvm.so(+0x1c4126) [0x7fab4f7f5126] + [bt] (1) /path/to/tvm/build/libtvm.so(+0x1ba2f8) [0x7fab4f7eb2f8] + [bt] (0) /path/to/tvm/build/libtvm.so(+0x177d12) [0x7fab4f7a8d12] + File "/path/to/tvm/src/api/api_test.cc", line 80 +ValueError: Check failed: x == y (0 vs. 1) : expect x and y to be equal. +>>> +>>> tvm.testing.ErrorTest(1, 1) +Traceback (most recent call last): + File "", line 1, in + File "/path/to/tvm/python/tvm/_ffi/_ctypes/function.py", line 190, in __call__ + raise get_last_ffi_error() +tvm.error.InternalError: Traceback (most recent call last): + [bt] (3) /path/to/tvm/build/libtvm.so(TVMFuncCall+0x48) [0x7fab500b8ca8] + [bt] (2) /path/to/tvm/build/libtvm.so(+0x1c4126) [0x7fab4f7f5126] + [bt] (1) /path/to/tvm/build/libtvm.so(+0x1ba35c) [0x7fab4f7eb35c] + [bt] (0) /path/to/tvm/build/libtvm.so(+0x177d12) [0x7fab4f7a8d12] + File "/path/to/tvm/src/api/api_test.cc", line 83 +InternalError: cannot reach here +TVM hint: You hit an internal error. Please open a thread on https://discuss.tvm.ai/ to report it. +``` + +如上面的示例所示,TVM 的 ffi 系统将 Python 和 C++ 的 StackTrace 合并为一条消息,并自动生成相应的错误类。 + +## 如何选择错误类型 + +可以浏览下面列出的错误类型,试着用常识并参考已有代码中的选择来决定。我们尽量将错误类型保持在一个合理的数目。如果你觉得需要添加新的错误类型,请这样做: + +- 在当前代码库中发送带有描述和使用示例的 RFC 提案。 +- 使用清晰的文档将新的错误类型添加到 [tvm.error](https://tvm.apache.org/docs/reference/api/python/error.html#module-tvm.error)。 +- 将新的错误类型添加到此文件中的列表里。 +- 在代码里使用新的错误类型。 + +创建简短的错误消息时推荐使用较少的抽象。代码以这种方式更具可读性,并且在必要时还打开了制作特定错误消息的路径。 + +``` python +def preferred(): + # 清楚知道抛出什么类型的错误以及错误消息是什么。 + raise OpNotImplemented("Operator relu is not implemented in the MXNet frontend") + +def _op_not_implemented(op_name): + return OpNotImplemented("Operator {} is not implemented.").format(op_name) + +def not_preferred(): + # In引入另一个间接方法 + raise _op_not_implemented("relu") +``` + +如果需要引入构造多行错误消息的包装函数,请将包装器放在同一个文件中,以便其他开发者可以轻松找到。 diff --git a/versioned_docs/version-0.12.0/contribute/git_howto.md b/versioned_docs/version-0.12.0/contribute/git_howto.md new file mode 100644 index 00000000..e96ca78a --- /dev/null +++ b/versioned_docs/version-0.12.0/contribute/git_howto.md @@ -0,0 +1,101 @@ +--- +title: Git 使用技巧 +sidebar_position: 7 +--- + +以下是 Git 工作流程的一些技巧。 + +## 如何解决与 `main` 的冲突 + +- 首先 rebase 到最近的 main + + ``` bash + # 前两步如果操作过可以跳过 + git remote add upstream [url to tvm repo] + git fetch upstream + git rebase upstream/main + ``` + +- Git 会显示一些无法 merge 的冲突,比如 `conflicted.py`。 + + - 手动修改文件以解决冲突。 + + - 解决冲突后,将其标记为已解决 + + ``` bash + git add conflicted.py + ``` + +- 然后可以通过以下命令继续 rebase + + ``` bash + git rebase --continue + ``` + +- 最后强制 push 到你的分支 + + ``` bash + git push --force + ``` + +## 如何将多个 commit merge 为一个 + +要将多个 commit(尤其是后面的 commit 只是对前面 commit 的修改时)组合为一个 PR 可以按照以下步骤操作。 + +- 如果之前没有配置过 Git 的默认编辑器,请先进行配置 + + ``` bash + git config core.editor the-editor-you-like + ``` + +- 假设要 merge 最新 3 个 commit,请输入以下命令 + + ``` bash + git rebase -i HEAD~3 + ``` + +- 它将弹出一个文本编辑器。将第一个 commit 设置为 `pick`,并将稍后的 commit 更改为 `squash`。 +- 保存文件后,它会弹出另一个文本编辑器,要求修改 merge 的 commit 消息。 + +- 将更改强制 push 到你的分支。 + + ``` bash + git push --force + ``` + +## 重置到最近的主分支 + +可用 git reset 重置为最新的主版本。注意,**所有本地更改都将丢失**。因此,只有在没有本地更改或 pull request 刚刚 merge 时才可以这样做。 + +``` bash +git fetch origin main +git reset --hard FETCH_HEAD +``` + +## 重置后恢复先前的 commit + +有时我们可能会将分支重置为错误的 commit。发生这种情况时,可以使用以下命令显示最近 commit 的列表 + +``` bash +git reflog +``` + +获得正确的 hashtag 后,可以再次使用 git reset 将 head 更改为正确的 commit。 + +## 仅将 k 个最新 commit 应用于 main + +当有其他 m 个 commit 在 k 个最新 commit 之前已经 merge 时,只在 main 上应用这 k 个最新 commit 很有用。直接对 main 进行 rebase 可能会导致这 m 个 commit 的 merge 冲突(可以安全地丢弃)。 + +可以改用以下命令: + +``` bash +# k 是实数 +# 将 HEAD~2 作为最新一个提交 +git rebase --onto upstream/main HEAD~k +``` + +然后可以强制 push 到 main。注意,上述命令将丢弃最后 k 个 commit 之前的所有 commit。 + +## 强制 push 的后果是什么 + +前两个技巧需要强制 push,这是因为我们改变了 commit 的路径。如果更改的 commit 仅属于你自己,就可以强制 push 到你自己的分支。 diff --git a/versioned_docs/version-0.12.0/contribute/pull_request.md b/versioned_docs/version-0.12.0/contribute/pull_request.md new file mode 100644 index 00000000..04cf8da1 --- /dev/null +++ b/versioned_docs/version-0.12.0/contribute/pull_request.md @@ -0,0 +1,134 @@ +--- +title: 提交 Pull Request +sidebar_position: 2 +--- + +## 准则 + +- 建议作者发送范围明确的 PR,以便在出现问题的时候便于 review 和 revert。因此,作者应该避免将多个不相关的修改 merge 到一个 PR 中。 +- 提交 PR 之前,请将代码重新建立在 `main` 的最新版本上,运行以下代码: + + ``` bash + git remote add upstream [url to tvm repo] + git fetch upstream + git rebase upstream/main + ``` + +- 确保代码通过 lint 检查 + + ``` bash + # 虽然使用的 lint 命令应该与在 CI 中运行的相同,但此命令会重现 + # 准确的 CI lint 过程(通常有助于调试 lint 脚本错误或避免手动安装工具) + python tests/scripts/ci.py lint + + # 运行所有的 lint 步骤。 + docker/lint.sh + + # 要单独运行步骤,请在命令行中指定步骤名称。 + # 一个不正确拼写的步骤名称会导致工具打印所有可用的步骤。 + docker/lint.sh ... + ``` + + 若 clang-format lint 检查失败,运行 git-clang-format 自动对代码重新格式化: + + ``` bash + # 运行 clang-format 检查所有自 upstream/main 以来改变的文件 + docker/bash.sh ci_lint ./tests/lint/git-clang-format.sh upstream/main + ``` + +- 添加测试用例,以涵盖补丁所引入的新功能或错误修复。 + +- 记录新写入的代码,更多信息请见 [文档](document) + +- 创建一个 [pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request),修复 CI 检查报告的问题。 + +- 请求其他贡献者帮你 review 代码,并根据其在 pull request 中用 `@` 标记的评论,来改进补丁。PR 标题中的标签会自动标记订阅用户,所以请确保在 PR 标题中加入并强调相关主题(例如:`[microTVM] 一个很酷的变化`,而不是 `microTVM 的一个很酷的变化`)。 + + - 为了加快代码 review 进程,推荐 committer 之间彼此帮助 review 代码。 + - 代码 review 是一个引导过程,有助于提高贡献者的代码质量。我们应该积极主动地对待它,在 review 前尽可能地改进代码。我们高度重视那些不需要大量 review 就能进入的补丁。 + - 详细的指导方针,并总结实用的经验。 + +- reviewer 批准 PR 后,才可以 merge。 + +## CI 环境 + +使用 Docker 镜像,创建能部署到多台机器上的稳定的 CI 环境。按照 [这个 issue 模板](https://github.com/apache/tvm/issues/new?assignees=&labels=&template=ci-image.md&title=%5BCI+Image%5D+) 的步骤,更新 CI Docker 镜像。 + +## 测试 + +尽管每个 pull request 都有自动运行单元测试的 hook,但推荐先在本地运行单元测试,以减少 reviewer 的负担并加快 review 过程。 + +### Docker(推荐) + +`tests/scripts/ci.py` 在本地复制 CI 环境,并提供一个用户友好界面。在 CI 中使用的 Docker 镜像和脚本,可以直接用于运行测试。它是在不同的文件夹中保存构建的,这便于维护多个测试环境,无需每次都从头开始重建(例如,你可以测试 CPU 和 i386 的变化,同时保留增量重建)。 + +``` bash +# 查看所有可用的平台 +python tests/scripts/ci.py --help +python tests/scripts/ci.py cpu --help + +# 在 ci_cpu docker 容器中运行 CPU 构建(构建将被留在 build-cpu/ 文件夹中) +# 注意:CPU 和 GPU 的 Docker 镜像相当大,可能在第一次使用时需要一些时间来下载 +python tests/scripts/ci.py cpu + +# 在 ci_cpu docker 容器中运行 CPU 构建,然后运行 unittests +python tests/scripts/ci.py cpu --unittest + +# 通过运行特定的测试快速迭代,并跳过每次的重建 +python tests/scripts/ci.py cpu --skip-build --tests tests/python/unittest/test_tir_transform_inject_rolling_buffer.py::test_upscale + +# 运行 CPU 构建,并将其放入容器中的一个 shell 中 +python tests/scripts/ci.py cpu --interactive +``` + +我们会定期更新 Docker 镜像,随着时间的推移,陈旧的镜像可能造成磁盘空间的浪费。您可以使用以下命令删除当前检出的分支以及任何其他工作树中未使用的陈旧 Docker 镜像: + +``` bash +docker/clear-stale-images.sh +``` + +有关更多选项,请参阅 --help。 + +### C++(本地) + +运行 C++ 测试需要安装 gtest,按照 [启用 C++ 测试](/docs/install/from_source#C++_tests) 中的说明进行安装 + +``` bash +# 假设您是在 tvm 源码根目录下 +TVM_ROOT=`pwd` + +./tests/scripts/task_cpp_unittest.sh +``` + +### Python(本地) + +必要的依赖: + +``` bash +pip install --user pytest Cython synr +``` + +如果希望运行所有的测试: + +``` bash +# 构建 tvm +make + +./tests/scripts/task_python_unittest.sh +``` + +如果希望运行单个测试: + +``` bash +# 构建 tvm +make + +# 让 python 知道在哪里可以找到 tvm 相关的库 +export PYTHONPATH=python +rm -rf python/tvm/*.pyc python/tvm/*/*.pyc python/tvm/*/*/*.pyc + +TVM_FFI=ctypes python -m pytest -v tests/python/unittest/test_pass_storage_rewrite.py + +# 另外,如果您想运行单一的测试,例如在一个文件内的 test_all_elemwise。 +TVM_FFI=ctypes python -m pytest -v -k "test_all_elemwise" tests/python/frontend/tflite/test_forward.py +``` diff --git a/versioned_docs/version-0.12.0/contribute/release_process.md b/versioned_docs/version-0.12.0/contribute/release_process.md new file mode 100644 index 00000000..8996320f --- /dev/null +++ b/versioned_docs/version-0.12.0/contribute/release_process.md @@ -0,0 +1,164 @@ +--- +title: 发版流程 +sidebar_position: 9 +--- + +TVM 中的版本 manager 角色意味着要负责以下这些事情: + +* 准备发行说明 +* 准备设置 +* 准备候选版本 + * 截取版本分支 + * 通知社区时间 + * 在分支中做必要版本更新的代码修改 +* 对版本投票 + * 创建候选版本 + * 召集投票和分类问题 +* 完成并发布版本 + * 更新 TVM 网站 + * 完成发行说明 + * 宣布发布 + +## 准备发行说明 + +发行说明包含新功能、改进、错误修复、已知问题和弃用等。TVM 提供 [每月开发报告](https://discuss.tvm.ai/search?q=TVM%20Monthly%20%23Announcement),收集每个月的开发进度。发行说明的编写者可能用得上这个。 + +建议在截取版本分支之前开一个 GitHub Issue 来收集发行说明初稿的反馈。 + +## 准备 GPG 密钥 + +如果已经上传了密钥,则可以跳过这部分。 + +参考 [https://www.apache.org/dev/openpgp.html#generate-key](https://www.apache.org/dev/openpgp.html#generate-key) 将生成的 gpg 密钥上传到公钥服务器。 + +通过 `gpg --export` 和 `gpg --import` 命令可以将 gpg 密钥传输到另一台机器上发布。 + +最后一步是使用你的代码签名密钥 [https://www.apache.org/dev/openpgp.html#export-public-key](https://www.apache.org/dev/openpgp.html#export-public-key) 更新 KEYS 文件。查看对 TVM 主分支及 ASF SVN 的更改, + +``` bash +# 指定 --depth=files 参数将跳过检查已有文件夹 +svn co --depth=files "https://dist.apache.org/repos/dist/dev/tvm" svn-tvm +cd svn-tvm +# 编辑 KEY 文件 +svn ci --username $ASF_USERNAME --password "$ASF_PASSWORD" -m "Update KEYS" +# 更新 downloads.apache.org +svn rm --username $ASF_USERNAME --password "$ASF_PASSWORD" https://dist.apache.org/repos/dist/release/tvm/KEYS -m "Update KEYS" +svn cp --username $ASF_USERNAME --password "$ASF_PASSWORD" https://dist.apache.org/repos/dist/dev/tvm/KEYS https://dist.apache.org/repos/dist/release/tvm/ -m "Update KEYS" +``` + +## 截取一个候选版本 + +要截取一个候选版本,首先用选定的版本字符串截取一个分支,例如, + +``` bash +git clone https://github.com/apache/tvm.git +cd tvm/ +git branch v0.6.0 +git push --set-upstream origin v0.6.0 +``` + +(*确保源代码中的版本号正确。*运行 `python3 version.py` 进行版本更新。) + +转到 GitHub 仓库的 "releases" 选项卡,然后单击 "Draft a new release", + +- 以 “v1.0.0.rc0” 的形式提供发布标签,其中 0 表示它是第一个候选版本 +- 单击 Target 选择提交:branch \> Recent commits \> \$commit_hash +- 将发行说明初稿复制并粘贴到说明框中 +- 选择 “This is a pre-release” +- 点击 “Publish release” + +注意:截取后 BRANCH 仍可以更改,而 TAG 是固定的。如果此版本要做任何更改,则必须创建一个新的 TAG。 + +删除以前的候选版本(如果有的话), + +``` bash +git push --delete origin v0.6.0.rc1 +``` + +创建源代码工程, + +``` bash +git clone git@github.com:apache/tvm.git apache-tvm-src-v0.6.0.rc0 +cd apache-tvm-src-v0.6.0.rc0 +git checkout v0.6 +git submodule update --init --recursive +git checkout v0.6.0.rc0 +rm -rf .DS_Store +find . -name ".git*" -print0 | xargs -0 rm -rf +cd .. +brew install gnu-tar +gtar -czvf apache-tvm-src-v0.6.0.rc0.tar.gz apache-tvm-src-v0.6.0.rc0 +``` + +使用 GPG 密钥对创建的工程进行签名。首先确保 GPG 使用正确的私钥, + +``` bash +$ cat ~/.gnupg/gpg.conf +default-key F42xxxxxxxxxxxxxxx +``` + +创建 GPG 签名以及文件的哈希, + +``` bash +gpg --armor --output apache-tvm-src-v0.6.0.rc0.tar.gz.asc --detach-sig apache-tvm-src-v0.6.0.rc0.tar.gz +shasum -a 512 apache-tvm-src-v0.6.0.rc0.tar.gz > apache-tvm-src-v0.6.0.rc0.tar.gz.sha512 +``` + +## 上传候选版本 + +编辑 GitHub 上的发布页面并上传前面步骤创建的工程。 + +版本 manager 还需要将工程上传到 ASF SVN, + +``` bash +# 指定 --depth=files 参数将跳过检查已有文件夹 +svn co --depth=files "https://dist.apache.org/repos/dist/dev/tvm" svn-tvm +cd svn-tvm +mkdir tvm-v0.6.0-rc0 +# 将文件复制到其中 +svn add tvm-0.6.0-rc0 +svn ci --username $ASF_USERNAME --password "$ASF_PASSWORD" -m "Add RC" +``` + +## 对候选版本投票 + +第一次投票在 Apache TVM 开发者名单([dev@tvm.apache.org](mailto:dev@tvm.apache.org))上进行。为了获得更多关注,可以创建一个以 “[VOTE]” 开头的 GitHub Issue,它会自动镜像到 dev@。可以查看以前的投票帖子来了解它是如何进行的。电子邮件应遵循以下格式: + +- 在电子邮件中提供版本说明初稿的链接 +- 提供候选版本工程的链接 +- 确保电子邮件为文本格式且链接正确 + +对于 dev@ 投票,必须至少有 3 个约束性 +1 投票,并且 +1 投票必须多于 -1 投票。投票完成后,发送一封包含总数的摘要电子邮件,主题类似于 \[VOTE\]\[RESULT\] \...。 + +在 ASF 中,投票“至少”开放 72 小时(3 天)。如果在这段时间内没有获得足够数量的约束性投票,将无法在投票截止日期关闭它,需要延期投票。 + +如果投票失败,社区需要相应地修改版本,创建新的候选版本并重新投票。 + +## 发布版本 + +投票通过后,要将二进制文件上传到 Apache 镜像,请将二进制文件从 dev 目录(这应该是它们被投票的地方)移动到发布目录。这种“移动”是将内容添加到实际发布目录的唯一方法。 (注:只有 PMC 可以移动到发布目录) + +``` bash +export SVN_EDITOR=vim +svn mkdir https://dist.apache.org/repos/dist/release/tvm +svn mv https://dist.apache.org/repos/dist/dev/tvm/tvm-v0.6.0-rc2 https://dist.apache.org/repos/dist/release/tvm/tvm-v0.6.0 + +# 如果您已将签名密钥添加到 KEYS 文件中,请同时更新发布副本。 +svn co --depth=files "https://dist.apache.org/repos/dist/release/tvm" svn-tvm +curl "https://dist.apache.org/repos/dist/dev/tvm/KEYS" > svn-tvm/KEYS +(cd svn-tvm && svn ci --username $ASF_USERNAME --password "$ASF_PASSWORD" -m"Update KEYS") +``` + +记得在 GitHub 上创建一个新版本 TAG(本例中为 v0.6.0)并删除预发布候选 TAG。 + +``` bash +git push --delete origin v0.6.0.rc2 +``` + +## 更新 TVM 网站 + +网站仓库位于 [https://github.com/apache/tvm-site](https://github.com/apache/tvm-site)。向下载页面中添加版本工程以及 GPG 签名和 SHA 哈希。 + +## 发布公告 + +向 [announce@apache.org](mailto:announce@apache.org) 和 [dev@tvm.apache.org](mailto:dev@tvm.apache.org) 发送公告邮件。公告应包括发布说明和下载页面的链接。 diff --git a/versioned_docs/version-0.12.0/contribute_idx.md b/versioned_docs/version-0.12.0/contribute_idx.md new file mode 100644 index 00000000..2947cd8b --- /dev/null +++ b/versioned_docs/version-0.12.0/contribute_idx.md @@ -0,0 +1,29 @@ +--- +title: 贡献指南 +slug: /contribute +sidebar_position: 2 +--- + +TVM 是由社区成员开发的,欢迎大家为 TVM 项目做出贡献。我们重视所有形式的贡献,包括但不限于: + +- 现有补丁的代码 review +- 文档和使用示例 +- 社区参与论坛和 issue +- 代码的可读性和开发手册 + - 为代码添加注释来提高可读性 + - 贡献文档来解释内部的设计选择 +- 测试用例,使代码库更加稳健 +- 推广项目的教程、博文和讲座 + +以下是对项目各方面的贡献指南: + +* [TVM 社区指南](contribute/community) +* [提交 Pull Request](contribute/pull_request) +* [代码 Review](contribute/code_review) +* [Committer 指南](contribute/committer_guide) +* [文档指南](contribute/document) +* [代码指南及 Tips](contribute/code_guide) +* [Git 使用技巧](contribute/git_howto) +* [使用 TVM 的 CI](contribute/ci) +* [发布流程](contribute/release_process) +* [错误处理指南](contribute/error_handling) diff --git a/versioned_docs/version-0.12.0/dev/_category_.json b/versioned_docs/version-0.12.0/dev/_category_.json new file mode 100644 index 00000000..89db347c --- /dev/null +++ b/versioned_docs/version-0.12.0/dev/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "开发者向导", + "position": 400 +} diff --git a/versioned_docs/version-0.12.0/dev/how_to.md b/versioned_docs/version-0.12.0/dev/how_to.md new file mode 100644 index 00000000..86dea47a --- /dev/null +++ b/versioned_docs/version-0.12.0/dev/how_to.md @@ -0,0 +1,11 @@ +--- +title: 开发者指南 +--- + +本章节将详细介绍在各种 TVM 堆栈运行的技巧,具体包括: + +* [调试 TVM](how_to/debugging_tvm) +* [向 Relay 中添加算子](how_to/relay_add_op) +* [向 Relay 中添加 Compiler Pass](how_to/relay_add_pass) +* [向 TVM 中添加 Codegen](how_to/relay_bring_your_own_codegen) +* [Python Target 参数化](how_to/python_target_parametrization) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/dev/how_to/01-debugging_tvm.md b/versioned_docs/version-0.12.0/dev/how_to/01-debugging_tvm.md new file mode 100644 index 00000000..3db1201f --- /dev/null +++ b/versioned_docs/version-0.12.0/dev/how_to/01-debugging_tvm.md @@ -0,0 +1,31 @@ +--- +title: 调试 TVM +--- + +# 调试 TVM + +**注意**:本章节内容持续更新中,欢迎各位提交 PR 不断丰富。本文档旨在集中分享调试 TVM 的常用方法,排列顺序以使用频率为依据。 + +## VLOGging + +TVM 提供了一个冗余信息的记录工具,允许开发者在不影响运行中 TVM 二进制大小或 runtime 的同时,提交跟踪级别的调试信息。在代码中使用 VLOG: + +``` c++ +void Foo(const std::string& bar) { + VLOG(2) << "Running Foo(" << bar << ")"; + // ... +} +``` + +在这个例子中,传递给 `VLOG()` 的整数 `2` 表示冗余级别。级别越高,打印的日志就越多。一般来说,TVM 级别从 0 到 2,3 只用于极底层的核心运行时属性。VLOG 系统在启动时被配置为打印 `0` 到某个整数 `N` 之间的 VLOG 语句。`N` 可以按文件或全局来设置。 + +默认情况下(当采用适当的优化进行编译时),VLOG 不会打印或影响二进制的大小或runtime。启用 VLOGging,请执行以下操作: + + +1. 在 `config/cmake` 中,确保设置 `set(USE_RELAY_DEBUG ON)`。这个标志是用来启用 VLOGging 的。 +2. 通过 `TVM_LOG_DEBUG=` 启动 Python,其中 `>` 是一个形式为 `=` 的以逗号分隔的级别赋值列表。尤其需要注意: + * 特殊文件名 `DEFAULT` 为所有文件设置 VLOG 级别。 + * `>` 可以被设置为 `-1` 来禁用该文件的 VLOG。 + * `` 是在 TVM 仓库中相对于 `src/` 目录的 c++ 源文件的名字(例如 `.cc`,而不是 `.h`)。您不需要在指定文无论在指定文件路径时,是否提供 `src/`,VLOG 都可以正确解释路径。 + + diff --git a/versioned_docs/version-0.12.0/dev/how_to/02-relay_add_op.md b/versioned_docs/version-0.12.0/dev/how_to/02-relay_add_op.md new file mode 100644 index 00000000..8f6c8f2e --- /dev/null +++ b/versioned_docs/version-0.12.0/dev/how_to/02-relay_add_op.md @@ -0,0 +1,363 @@ +--- +title: 向 Relay 中添加算子 +--- + +# 向 Relay 中添加算子 + +本文档将以添加 [cumulative product](https://github.com/apache/tvm/pull/7722) 算子的 PR(基于 [cumulative sum](https://github.com/apache/tvm/pull/7334) 算子 PR)为例,介绍在 Relay 中注册一个新的 TVM 算子所需的步骤。 + +注册一个新的算子需要如下几个步骤: + +1. 添加一个属性节点,声明在编译时已知的固定参数 +2. 为算子编写一个类型关系,以整合到 Relay 的类型系统中 +3. 使用 C++ 中的 `RELAY_REGISTER_OP` 宏,为编译器注册算子的数量、类型和其他提示 +4. 编写算子的计算方式 +5. 用 Relay 注册算子和 schedule +6. 定义一个为算子产生调用节点的 C++ 函数,并为该函数注册一个 Python API hook +7. 将上述 Python API hook 放在一个更简洁的接口中 +8. 为新的 Relay 算子编写测试 + +## 1. 定义属性节点 + +属性是在编译时已知的固定参数。卷积算子的步长和扩张可能属于卷积算子属性节点字段的恰当示例。 + +属性应在文件夹 [include/tvm/relay/attrs/](https://github.com/apache/tvm/tree/main/include/tvm/relay/attrs) 内的文件中定义。 + +最终我们要创建一个算子,它的接口可以在最终的 Python 接口中清晰可见: + +``` python +def cumprod(data, axis=None, dtype=None, exclusive=None): + """Numpy style cumprod op. Return the cumulative inclusive product of the elements along a given axis. + 参数 + ---------- + data : relay.Expr 类型 + 算子的输入数据。 + axis : int 类型,可选 + Axis along which the cumulative product is computed. The default (None) is to compute the cumprod over the flattened array. + dtype : string 类型,可选 + Type of the returned array and of the accumulator in which the elements are multiplied. + 如果 dtype 没有被指定, 那么它默认为 data 的 dtype。 + exclusive : bool 类型,可选 + If true will return exclusive product in which the first element is not included. In other terms, if true, the j-th output element would be the product of the first (j-1) elements. Otherwise, it would be the product of the first j elements. The product of zero elements will be 1. + 返回 + ------- + result : relay.Expr 类型 + 如果 axis 不为空的话,结果的大小和形状和 data 一样。 + 如果 axis 为空的话, 结果是一个一维数组。 + """ +``` + +`cumsum()` 存在类似的接口。 + +因此,在 `include/tvm/relay/attrs/transform.h` 中定义属性时,可以选择算子的 axis、accumulation dtype 及 exclusivity 作为结构体的合适字段。 + +``` cpp +/*! 用在 cumsum 和 cumprod 算子中的简单属性 */ +struct ScanopAttrs : public tvm::AttrsNode { + Integer axis; + DataType dtype; + Bool exclusive = Bool(false); + TVM_DECLARE_ATTRS(ScanopAttrs, "relay.attrs.ScanopAttrs") { + TVM_ATTR_FIELD(axis).describe("The axis to operate over").set_default(NullValue()); + TVM_ATTR_FIELD(dtype).describe("Output data type").set_default(NullValue()); + TVM_ATTR_FIELD(exclusive) + .describe("The first element is not included") + .set_default(Bool(false)); + } +}; +``` + +## 2. 编写类型关系 + +为了提高注册算子的灵活性,在 Relay 中表示类型时更突出,算子使用输入和输出类型之间的关系进行类型化。这些关系被表示为函数,它接收一个输入类型和输出类型的列表(这些类型中的任何一个都可能是不完整的),然后返回一个满足关系的输入和输出类型的列表,包括可以在编译时静态确定的形状信息。基本上,一个算子的关系除了计算输出类型外,还可以执行所有必要的类型化规则(即通过检查输入类型)。 + +在 `src/relay/op/tensor/transform.cc` 中可以找到 cumulative product 与 cumulative product 算子的类型关系。 + +``` cpp +TVM_REGISTER_NODE_TYPE(ScanopAttrs); +bool ScanopRel(const Array& types, int num_inputs, const Attrs& attrs, const TypeReporter& reporter) { + // types: [data, output] + ICHECK_EQ(types.size(), 2) << "Expects two types, one for the input and another for the output"; + const auto* data = types[0].as(); + if (data == nullptr) { + ICHECK(types[0].as()) + << "Scanop: expect input type to be TensorType but get " << types[0]; + return false; + } + + const auto* param = attrs.as(); + + auto dtype = param->dtype; + if (dtype.is_void()) { + dtype = data->dtype; + } + + if (param->axis.defined()) { + reporter->Assign(types[1], TensorType(data->shape, dtype)); + } else { + auto prod = data->shape[0]; + for (size_t i = 1; i < data->shape.size(); ++i) { + prod = prod * data->shape[i]; + } + reporter->Assign(types[1], TensorType({prod}, dtype)); + } + + return true; +} +``` + +## 3. 将参数数量和属性与算子关联起来 + +注册新算子的名称,并为其添加调用接口的注解。C++ 中的 `RELAY_REGISTER_OP` 宏允许开发者在 Relay 中指定一个算子的以下信息: + +* 参数数量 +* 位置参数的名称和描述 +* 支持级别(1 表示内部固有的;更高的数字表示集成度低或外部支持的算子) +* 算子的类型关系 +* 其他在优化算子时有用的注解 + +再次将其添加到 `src/relay/op/tensor/transform.cc` 中: + +``` cpp +RELAY_REGISTER_OP("cumsum") + .describe( + R"doc(Return the cumulative sum of the elements along a given axis.)doc" TVM_ADD_FILELINE) + .set_num_inputs(1) + .add_argument("data", "Tensor", "The input tensor.") + .set_support_level(3) + .add_type_rel("Cumsum", ScanopRel) + .set_attr("TOpPattern", kOpaque); + +RELAY_REGISTER_OP("cumprod") + .describe( + R"doc(Return the cumulative product of the elements along a given axis.)doc" TVM_ADD_FILELINE) + .set_num_inputs(1) + .add_argument("data", "Tensor", "The input tensor.") + .set_support_level(3) + .add_type_rel("Cumprod", ScanopRel) + .set_attr("TOpPattern", kOpaque); +``` + +在这种情况下,`TOpPattern` 是对编译器关于算子执行的计算模式的提示,这对于融合算子可能很有用。`kOpaque` 提示 TVM 无需融合这个算子。 + +## 4. 定义算子的计算 + +为算子定义接口后,仍需定义如何执行 cumulative sum 和 cumulative product 的实际计算。 + +假设算子计算的实现方式,经过了多轮测试且表现良好。推荐查看 [张量表达式教程](/docs/tutorial/tensor_expr)、[TVM 算子清单(topi)](/docs/tutorial/TOPI)、[python/tvm/topi/scan.py](https://github.com/apache/tvm/blob/main/python/tvm/topi/scan.py) 中 cumulative sum 及 cumulative product 相关实现案例,以及 [python/tvm/topi/cuda/scan.py](https://github.com/apache/tvm/blob/main/python/tvm/topi/cuda/scan.py) 中的 GPU 版本。在 cumulative sum 及 cumulative product 算子中,可以直接用 [TIR](https://tvm.apache.org/docs/reference/api/python/tir.html#api-python-tir),张量表达式及 topi 降级后表示为 TIR。 + +## 5. 将计算(compute)和策略(strategy)与 Relay 关联起来 + +实现计算函数后,需要将其与 Relay 算子粘合在一起。在 TVM 中,这意味着不仅要定义 computation,还要定义算子的 schedule。策略决定使用哪种 computation 及 schedule。例如,对于二维卷积,识别出这属于一种深度卷积后,最终将其分配给一个更有效的 computation 和 schedule。 + +实际上除了在 CPU 和 GPU 的实现之间进行调度外,基本没有类似需求。在 `python/tvm/relay/op/strategy/generic.py` 和 `python/tvm/relay/op/strategy/cuda.py` 中,我们添加了如下策略: + +``` python +def wrap_compute_scanop(topi_compute): + """Wrap scanop style topi compute""" + + def _compute_scanop(attrs, inputs, _): + return [topi_compute(inputs[0], attrs.axis, attrs.dtype, attrs.exclusive)] + + return _compute_scanop + +@override_native_generic_func("cumsum_strategy") +def cumsum_strategy(attrs, inputs, out_type, target): + """cumsum 基本策略""" + strategy = _op.OpStrategy() + strategy.add_implementation( + wrap_compute_scanop(topi.cumsum), + wrap_topi_schedule(topi.generic.schedule_extern), + name="cumsum.generic", + ) + return strategy + +@override_native_generic_func("cumprod_strategy") +def cumprod_strategy(attrs, inputs, out_type, target): + """cumprod 基本策略""" + strategy = _op.OpStrategy() + strategy.add_implementation( + wrap_compute_scanop(topi.cumprod), + wrap_topi_schedule(topi.generic.schedule_extern), + name="cumprod.generic", + ) + return strategy + +@cumsum_strategy.register(["cuda", "gpu"]) +def cumsum_strategy_cuda(attrs, inputs, out_type, target): + """cumsum cuda 策略""" + strategy = _op.OpStrategy() + strategy.add_implementation( + wrap_compute_scanop(topi.cuda.cumsum), + wrap_topi_schedule(topi.cuda.schedule_scan), + name="cumsum.cuda", + ) + return strategy + +@cumprod_strategy.register(["cuda", "gpu"]) +def cumprod_strategy_cuda(attrs, inputs, out_type, target): + """cumprod cuda 策略""" + strategy = _op.OpStrategy() + strategy.add_implementation( + wrap_compute_scanop(topi.cuda.cumprod), + wrap_topi_schedule(topi.cuda.schedule_scan), + name="cumprod.cuda", + ) + return strategy +``` + +每个策略都定义了写入的 compute 以及在 `add_implementation()` 中使用的 schedule。最后,将 strategy 和 compute 与`python/tvm/relay/op/_transform.py` 中定义的 Relay 算子关联起来。 + +``` python +# cumsum +@_reg.register_compute("cumsum") +def compute_cumsum(attrs, inputs, output_type): + """cumsum 的计算定义""" + return [topi.cumsum(inputs[0], attrs.axis, attrs.dtype, attrs.exclusive)] + +_reg.register_strategy("cumsum", strategy.cumsum_strategy) +_reg.register_shape_func("cumsum", False, elemwise_shape_func) + +# cumprod +@_reg.register_compute("cumprod") +def compute_cumprod(attrs, inputs, output_type): + """cumprod 的计算定义""" + return [topi.cumprod(inputs[0], attrs.axis, attrs.dtype, attrs.exclusive)] + +_reg.register_strategy("cumprod", strategy.cumprod_strategy) +_reg.register_shape_func("cumprod", False, elemwise_shape_func) +``` + +shape 函数用于确定 output shape,给定一个动态 shaped tensor。在这种情况下,TVM 的 output shape 与 input shape 保持一致。 + +## 6. 创建 Relay 调用节点并提供 Python Hook + +现在已经有了一个可以运行的算子,接下来只需通过一个 Relay 调用节点(Relay Call Node)正确地调用即可。这一步需要简单地编写一个函数,接收算子的参数(作为 Relay 表达式),并向算子返回一个的调用节点(即应该被放在调用算子的 Relay AST 中的节点)。 + +目前不支持调用属性和类型参数(最后两个字段),所以只需使用 `Op::Get` 从算子注册表中获取算子信息,并将参数传递给调用节点(如下所示)。在 `src/relay/op/tensor/transform.cc`: + +``` cpp +Expr MakeCumsum(Expr data, Integer axis, DataType dtype, Bool exclusive) { + auto attrs = make_object(); + attrs->dtype = dtype; + attrs->axis = axis; + attrs->exclusive = exclusive; + static const Op& op = Op::Get("cumsum"); + return Call(op, {data}, Attrs(attrs), {}); +} + +TVM_REGISTER_GLOBAL("relay.op._make.cumsum").set_body_typed(MakeCumsum); + +Expr MakeCumprod(Expr data, Integer axis, DataType dtype, Bool exclusive) { + auto attrs = make_object(); + attrs->dtype = dtype; + attrs->axis = axis; + attrs->exclusive = exclusive; + static const Op& op = Op::Get("cumprod"); + return Call(op, {data}, Attrs(attrs), {}); +} + +TVM_REGISTER_GLOBAL("relay.op._make.cumsum").set_body_typed(MakeCumprod); +``` + +其中 `TVM_REGISTER_GLOBAL` 通过 `relay.op._make.cumsum(...)` 和 `relay.op._make.cumsum(...)` 分别暴露(expose)Python 中的 `MakeCumsum` 和 `MakeCumprod` 函数。 + +## 7. 包含一个更简洁的 Python API hook + +通常 Relay 中约定俗成的是,通过 `TVM_REGISTER_GLOBAL` 导出的函数应该包装在单独的 Python 函数中,而不是直接在 Python 中调用。对于算子,我们在 `python/tvm/relay/op/transform.py` 中提供了更简洁的接口: + +``` python +def cumsum(data, axis=None, dtype=None, exclusive=None): + return _make.cumsum(data, axis, dtype, exclusive) + +def cumprod(data, axis=None, dtype=None, exclusive=None): + return _make.cumprod(data, axis, dtype, exclusive) +``` + +注意,这些 Python wrapper 也可能为算子提供更简洁的接口。例如 `concat` 算子被注册为只接受一个算子(即一个带有要连接的张量的元组),但是 Python wrapper 将张量作为参数,并在产生调用节点之前将它们组合成一个元组。 + +``` python +def concat(*args): + """围绕零轴连接输入张量。 + + 参数 + ---------- + args: Tensor 列表 + + 返回 + ------- + tensor: 连接的张量。 + """ + tup = Tuple(list(args)) + return _make.concat(tup) +``` + +## 8. 编写单元测试 + +更多用于 cumulative sum 和 cumulative product 算子的单元测试示例,请查看 [tests/python/relay/test_op_level3.py](https://github.com/apache/tvm/blob/main/tests/python/relay/test_op_level3.py)。 + +# 其他主题 + +## 梯度算子 + +梯度算子对于在 Relay 中编写可微分程序很重要。虽然 Relay 的 autodiff 算法可以得到优秀的语言结构的微分,但算子是不透明的。因为 Relay 无法查看它的实现,所以必须提供明确的微分规则。 + +Python 和 C++ 都可用于编写梯度算子,这里重点介绍更为常用的 Python 实例。 + +## 在 Python 中添加梯度算子 + +Python 梯度算子集合可以在 `python/tvm/relay/op/_tensor_grad.py` 中找到 。本部分内容将详细介绍两个有代表性的例子:`sigmoid` 和 `multiply`。 + +``` python +@register_gradient("sigmoid") +def sigmoid_grad(orig, grad): + """返回 [grad * sigmoid(x) * (1 - sigmoid(x))].""" + return [grad * orig * (ones_like(orig) - orig)] +``` + +这里的输入是原始算子 `orig` 以及梯度算子 `grad`,返回是一个列表,其中第 i 个索引的元素,是算子相对于算子第 i 个输入的导数。通常,梯度算子将返回一个列表,其元素的个数和基础算子(base operator)的输入一样多。 + +进一步分析这个定义之前,先回忆一下 sigmoid 函数的导数:∂σ/∂x=σ(x)(1−σ(x))。上面的定义看起来类似于数学定义,但有一个重要的补充: + +术语 `orig * (ones_like(orig) - orig)` 直接匹配导数,因为这里的 `orig` 是 sigmoid 函数。除了要了解如何计算该函数的梯度之外,还要掌握该梯度与其他梯度组合的方法,即在整个程序中累积梯度。这就是 `grad` 的作用。在表达式 `grad * orig * (ones_like(orig) - orig)`中,乘以 `grad` 指定了到目前为止如何用梯度组合导数。 + +接下来请看 `multiply`的示例: + +``` python +@register_gradient("multiply") +def multiply_grad(orig, grad): + """返回 [grad * y, grad * x]""" + x, y = orig.args + return [collapse_sum_like(grad * y, x), + collapse_sum_like(grad * x, y)] +``` + +在此示例中,返回列表中有两个元素,因为 `multiply` 是二元运算符(binary operator)。如果 f(x,y) = xy,偏导数是 ∂f / ∂x = y 和 ∂f / ∂y = x。 + +与 `sigmoid` 相比,`multiply` 需要一个额外的步骤,因为 `multiply` 具有广播语义(broadcasting semantics)。由于 `grad` 的 shape 可能与输入 shape 不匹配,所以我们使用 `collapse_sum_like` 来获取 `grad * ` 项的内容,并使其 shape 与做微分的输入 shape 相匹配。 + +## 在 C++ 中添加梯度算子 + +在 C++ 中添加梯度算子的方法,与在 Python 中添加梯度算子类似,但注册的接口略有不同。 + +首先,确保 `src/relay/transforms/pattern_utils.h` 被包含在内。它提供了用于在 Relay AST 中创建节点的辅助函数。定义梯度算子的方式与 Python 类似: + +``` cpp +tvm::Array MultiplyGrad(const Expr& orig_call, const Expr& output_grad) { + const Call& call = orig_call.Downcast(); + return { CollapseSumLike(Multiply(output_grad, call.args[1]), call.args[0]), + CollapseSumLike(Multiply(output_grad, call.args[0]), call.args[1]) }; +} +``` + +注意,在 C++ 中不能使用与 Python 相同的运算符重载(operator overloading),而是需要向下转换,因此实现更加冗长。即便如此,我们仍然可以轻易地验证这个定义反映了 Python 中先前的例子。 + +要注册梯度算子,这里无需使用 Python 修饰器,只需要在基础算子注册的末尾添加 `set_attr` 调用 "FPrimalGradient" 即可。 + +``` cpp +RELAY_REGISTER_OP("multiply") + // ... + // 设置其他属性 + // ... + .set_attr("FPrimalGradient", MultiplyGrad); +``` diff --git a/versioned_docs/version-0.12.0/dev/how_to/03-relay_add_pass.md b/versioned_docs/version-0.12.0/dev/how_to/03-relay_add_pass.md new file mode 100644 index 00000000..4087fda8 --- /dev/null +++ b/versioned_docs/version-0.12.0/dev/how_to/03-relay_add_pass.md @@ -0,0 +1,259 @@ +--- +title: 向 Relay 中添加 Compiler Pass +--- + +# 向 Relay 中添加 Compiler Pass + +Compiler Pass 是扩展 Relay 功能集及优化 Relay 程序的主要接口。通过编写 compiler pass,用户可以基于最终目标,修改 AST 或收集 AST 相关信息。事实上,Relay 内置的一些重要特性(如自动微分和类型推断)都“标准”的 compiler pass。 + +整体来看,编写 pass 包括两个关键组成部分: + +* 创建一个或多个遍历程序的 C++ 类 +* 将遍历实现及其在 pass manager API 中的元数据包装,从而方便与 [Pass Infrastructure](/docs/arch/arch/pass_infra) 轻松交互 + +首先,我们将概述编写 compiler pass 的关键机制。然后通过 Relay 中常量折叠 pass 的具体示例进行演示。 + +# AST 遍历器(Traversers) + +用于遍历 Relay 程序的基类是 `ExprFunctor`。它提供的公共接口是一个 `VisitExpr` 方法,该方法接收一个表达式以及零个或多个参数,并返回某种类型的实例。扩展此类时,可以通过覆盖每种表达式类型的 `VisitExpr_` 实现,来定义 AST 遍历模式。 + +`VisitExpr` 和 `VisitExpr_` 之间的关系与调度有关。每个 `VisitExpr_` 定义都针对特定类型的表达式,但用户无法每次都得知要访问的节点类型。为了解决这个问题,`ExprFunctor` 提供了一个 `VisitExpr` 函数,将给定表达式路由转换为 `VisitExpr_` 实例进而解决问题。尽管 C++ 已经提供了动态调度,但 `ExprFunctor` 定义了自己的虚表供 `VisitExp` 使用。通过定义虚表可以更好地控制调度。例如,定义一个在每次访问之前都打印 "Here" 的 `PrintVisitor` 遍历器,可以覆盖 `VisitExpr`: + +``` c++ +void PrintVisitor::VisitExpr(const Expr& expr) { + std::cout << "Here" << std::endl; + ExprFunctor::VisitExpr(expr); +} +``` + +`ExprFunctor` 本身是一个非常通用的类,这就是为什么更多时候你会扩展 `ExprVisitor` 或 `ExprMutator`。这些类扩展了 `ExprFunctor`,并提供了 `VisitExpr_` 的默认实现,这些实现捕获了每种表达式类型的常见遍历模式。有了这些默认的实现,开发者只需针对想要不同行为的表达式类型,提供覆盖的实现。后续章节将针对每个子类进行详细描述。 + +## 表达式访问器(Expression Visitors) + +`ExprVisitor` 不用于修改程序的pass,而是用于实施程序分析和收集信息的 pass。使用这个类,`VisitExpr` 和私有 counterparts 不会返回任何内容。此类提供的 `VisitExpr_` 实现只是访问表达式的所有表达式字段。 `IfNode` 的默认实现如下所示: + +``` c++ +void ExprVisitor::VisitExpr_(const IfNode* op) { + this->VisitExpr(op->cond); + this->VisitExpr(op->true_branch); + this->VisitExpr(op->false_branch); +} +``` + +注意,这里调用的是 `VisitExpr` 而非 `VisitExpr_`,因此用户可以使用 `ExprFunctor` 中的虚表进行路由。 + +如果要编写一个 `CallChecker` 类来检查程序中是否出现函数调用,只需扩展 `ExprVisitor` 并定义以下 `VisitExpr_` 方法: + +``` c++ +void VisitExpr_(const CallNode* n) final { + result_ = true; +} +``` + +其中 `result_` 是一个字段。在该示例中,无需在 `CallNode` 字段上进一步递归,因为 `result_` 已经为 true,原始表达式中包含一个调用。为了使该访问器可用,可以采用以下方法: + +``` c++ +bool Check(const Expr& expr) final { + result_ = false; + VisitExpr(expr); + return result_; +} +``` + +以上就是全部操作。在调用 top-level 的递归之前,定义一个执行一些记录的公有接口是很常见的操作。用户也可以通过创建一个生成 `CallChecker` 实例,并在其上调用 `Check` 的独立程序来进一步包装 API,重要的是用尽可能少的资源用实现目标。 + +## 表达式修改器(Expression Mutators) + +`ExprMutator` 用于以某种方式转换程序的 pass。通过这个类,`VisitExpr` 及其对应的私有部分返回 `Expr`。此类提供的默认 `VisitExpr_` 实现访问表达式的所有表达式字段,并将字段设置为访问它们的结果。`TupleGetItemNode` 的默认实现如下所示: + +``` c++ +Expr ExprMutator::VisitExpr_(const TupleGetItemNode* g) { + auto t = this->Mutate(g->tuple); + if (g->tuple == t) { + return GetRef(g); + } else { + return TupleGetItem(t, g->index); + } +} +``` + +这里有几点需要注意。首先,`Mutate` 是 `ExprMutator` 中 `VisitExpr` 的别名。其次,如果对 `Mutate` 的调用修改了 `tuple` 字段,则只返回一个新节点。这种更新的方法称为功能更新,这样做可以避免不必要的分配。 + +`ExprMutator` 有、而 `ExprVisitor` 没有的一个功能,是用于缓存结果的内置 `memo_` 字段。`ExprMutator` 有一个记忆器(memoizer)这是合理的,因为用户知道正在缓存哪些类型的结果(即 `Expr`),而 `ExprVisitor` 的访问方法不返回任何内容。通常,当用户要在 `ExprVisitor` 的子类中缓存结果时,需要自行定义缓存。 + +如果希望编写一个 `IfCollapser` 类,用它的真实分支替换每个 if 语句,用户将为 `IfNode` 覆盖 `VisitExpr_`: + +``` c++ +Expr ExprMutator::VisitExpr_(const IfNode* op) { + return this->Mutate(op->true_branch); +} +``` + +注意:返回的表达式不一定是 `IfNode`,这是正常的,因为返回类型是 `Expr`。接下来创建一个公有接口: + +``` c++ +Expr CollapseIfs(const Expr& expr) final { + return this->Mutate(expr); +} +``` + +虽然使用这个修改器无需做任何记录,但仍然鼓励用户将描述性方法作为接口。 + +# 示例:常量折叠 + +为了更好地理解编写 pass 的过程,本部分将以常量折叠 pass(可在 [src/relay/transforms/fold_constant.cc](https://github.com/apache/tvm/blob/main/src/relay/transforms/fold_constant.cc) 中找到)作为示例进行讲解。常量折叠 pass 相对简单,且包含两种类型的遍历。 + +常量折叠涉及只包含常量的程序评估表达式(evaluating expression),然后用评估它们的结果替换这些表达式。此过程的目的是预加载可以进行的所有计算。为了实现这一点,常量折叠 pass 使用了一个访问器(`ConstantChecker`)和一个修改器(`ConstantFolder`)。 + +## `ConstantChecker` 访问器 + +此访问器用于检查表达式是否为常量。在 Relay 中,用户将 `ConstantNode` 或者只有常量字段的 `TupleNode` 的表达式定义为常量。 + +使用 `memo_` 字段从节点映射到它们是否为常量,并缓存这些结果。下面是 `ConstantChecker` 中的 `VisitExpr_` 定义。 + +``` c++ +void VisitExpr_(const ConstantNode* n) final { + memo_[GetRef(n)] = true; +} + +void VisitExpr_(const TupleNode* n) final { + bool result = true; + for (const auto& field : n->fields) { + if (!Check(field)) { + result = false; + break; + } + } + memo_[GetRef(n)] = result; +} +``` + +用于协调这些定义的记录是一个 `Check` 方法,它返回给定的表达式是否被认定为常量。 + +``` c++ +bool Check(const Expr& expr) { + const auto it = memo_.find(expr); + if (it != memo_.end()) + return it->second; + VisitExpr(expr); + return memo_[expr]; +} +``` + +并不是所有遇到的节点都要修改 `memo_`;相反,用户只有在遇到的节点有可能是常数时,才修改 `memo_`。当 `memo_` 不包含 `expr` 时,需要依赖默认的 false 值。 + +## `ConstantFolder` 修改器 + +这个修改器执行了大部分的常量折叠过程,并在内部使用 `ConstantChecker`。在 Relay 中,常量折叠涉及三种节点类型:`LetNode`、`TupleItemGetNode` 和 `CallNode`。后续段落中将进行详细讲解。 + +``` c++ +Expr VisitExpr_(const LetNode* op) final { + Expr value = this->Mutate(op->value); + if (value.as()) { + memo_[op->var] = value; + return this->Mutate(op->body); + } else { + Var var = Downcast(this->Mutate(op->var)); + Expr body = this->Mutate(op->body); + if (var.same_as(op->var) && + value.same_as(op->value) && + body.same_as(op->body)) { + return GetRef(op); + } else { + return Let(var, value, body); + } + } +} +``` + +在 `LetNode` 示例里,首先尝试常量折叠绑定在表达式的值。如果可以,填充 `memo_` 并返回访问主体的结果——本质上是将绑定的值传到主体中的使用点。如果无法常量折叠绑定的值,可以参照默认的实现方法: + +``` c++ +Expr VisitExpr_(const TupleGetItemNode* op) final { + Expr res = ExprMutator::VisitExpr_(op); + op = res.as(); + if (const auto* tuple = op->tuple.as()) { + return tuple->fields[op->index]; + } else { + return res; + } +} +``` + +在 `TupleItemGetNode` 的例子里,需要检查 `op->tuple` 字段是否为 `TupleNode`。如果是,我们将 get 元组替换为 `op->index` 指向的元组的字段。这样做的原因是因为 `op->tuple` 可能被错误评估为一个元组。 + +``` c++ +Expr VisitExpr_(const CallNode* call) final { + static auto op_stateful = Op::GetAttrMap("TOpIsStateful"); + Expr res = ExprMutator::VisitExpr_(call); + call = res.as(); + // 我们不使用零参数的常量折叠函数。 + // 这是一个很有用的启发式方法。 + // 例如折叠那些 shape=(4, 5) 是有害的。 + if (call->args.size() == 0) return res; + const OpNode* op = call->op.as(); + if (op == nullptr) return res; + // 跳过有状态的算子。 + if (op_stateful.get(GetRef(op), false)) return res; + bool all_const_args = true; + for (Expr arg : call->args) { + if (!checker_.Check(arg)) { + all_const_args = false; + } + } + if (all_const_args) { + return ConstEvaluate(res); + } else { + return res; + } +} +``` + +在 `CallNode` 示例中,首先使用 `ExprMutator` 的 `VisitExpr_` 来访问调用,它将调用的所有字段都常量折叠了。之所以使用 `ExprMutator::VisitExpr_` 而不是 `VisitExpr`,是因为我们想要绕过虚表(以避免死循环)并使用 `ExprMutator` 提供的默认实现。只有当所有参数都是常量时,才评估调用(使用 `ConstantChecker`)。评估调用会产生一个**值**,因此这里使用辅助方法 `ValueToExpr` ,将评估的表达式放回 AST 中。 + +现在,我们为常量文件夹构造了一个更方便的接口 `FoldConstant`。`FoldConstant` 是 `ConstantFolder` 类之外的一个独立函数,它负责接收表达式并在内部创建和使用 `ConstantFolder` 实例(其完整的定义在 [src/relay/transforms/fold_constant.cc](https://github.com/apache/tvm/blob/main/src/relay/transforms/fold_constant.cc) 中)。 + +## 用 Pass Manager 注册 Pass + +*注意:更多详情请参阅 [Pass Infrastructure](../../arch/arch/pass_infra) 中的文档。 + +编写 AST 遍历器后,用以下代码可将 pass 注册为 TVM API 端点: + +``` c++ +namespace transform { + +Pass FoldConstant() { + runtime::TypedPackedFunc pass_func = + [=](Function f, Module m, PassContext pc) { + return Downcast(FoldConstant(f)); + }; + return CreateFunctionPass(pass_func, 2, "FoldConstant", {}); +} + +} // 命名空间转换 +``` + +将上述代码生成的 `Pass` 对象提供给 pass 基础架构,可以使得 AST 遍历应用于给定 Relay 模块中的所有函数,这是常量折叠过程预期的行为(它应该尽可能折叠所有常量)。 + +函数 `CreateFunctionPass` 允许注册 pass 的优化级别(在本例中为 2),可用于根据 pass 的一般实用性、 pass 名称和 pass 中的任何依赖项将 pass 组合在一起。pass 的依赖项以列表形式给出,罗列了当前 pass 运行所必需的所有 pass 的结果。`FoldConstant` 没有任何依赖,但是很多 Relay pass 确实依赖有类型信息,所以 `InferType` 是一个常见的依赖;其他的可能依赖于程序为 A-范式,通过 `ToANormalForm` pass。 + +注意,`PassContext` 对象包含 pass 用于错误报告和配置选项的信息; `FoldConstant` 不需要此信息,但其他 pass 可能会引用它们的 `PassContext` 对象。 + +现在可以通过 pass 基础结构调用 pass 了,推荐为 pass 添加 Python 绑定,如以下代码片段所示: + +``` c++ +TVM_REGISTER_GLOBAL("relay._transform.FoldConstant") +.set_body_typed(FoldConstant); +``` + +通过以上方法定义了 `Pass` 对象后,就可以用 pass 基础架构的 `Sequential` 结构来调用了。 `Sequential` 接收一个 pass 列表,并将其按顺序应用于 Relay 模块,从而获得转换后的模块。例如,下面的代码将 `FoldConstant` 和 `ToANormalForm` pass 逐一应用于 `mod` 中的每个函数,并获得一个新模块。 + +``` c++ +seq = transform.Sequential([ + relay.transform.FoldConstant(), + relay.transform.ToANormalForm() +]) +new_mod = seq(mod) +``` + +更多注册相关的内容,请查看 [TVM Runtime 系统](/docs/arch/arch/runtimes);pass 管理器接口相关的更多信息,请查看 [Pass 基础架构](/docs/arch/arch/pass_infra); Relay 的标准 pass 列表及实现方式,请分别查看 [include/tvm/relay/transform.h](https://github.com/apache/tvm/blob/main/include/tvm/relay/transform.h) 及 [src/relay/transforms/](https://github.com/apache/tvm/tree/main/src/relay/transforms)。 diff --git a/versioned_docs/version-0.12.0/dev/how_to/04-relay_bring_your_own_codegen.md b/versioned_docs/version-0.12.0/dev/how_to/04-relay_bring_your_own_codegen.md new file mode 100644 index 00000000..10ebc4ad --- /dev/null +++ b/versioned_docs/version-0.12.0/dev/how_to/04-relay_bring_your_own_codegen.md @@ -0,0 +1,926 @@ +--- +title: 向 TVM 中添加 Codegen +--- + +# 向 TVM 中添加 Codegen + +随着深度学习工作负载所针对的硬件设备数量不断增加,用户在各种设备上实现高性能所需的知识也在不断增加。为了让数据科学家在开发新模型时不必担心性能问题,硬件厂商或是基于一些常见的深度学习算子,提供 MKLDNN 或 cuDNN 等库,或是提供 TensorRT 等框架,让用户按照某种方式描述模型,从而提高模型性能。 + +然而,用户在尝试使用新的库或设备时,必须学习新的编程接口。因此,一个统一的编程接口变得越来越重要:1)让所有用户及硬件厂商信息同步,2)提供一个可行的解决方案,让特定硬件或库只支持具有极高性能的、广泛使用的算子,不受支持的算子则回退到 CPU/GPU 等通用设备。 + +本开发手册演示了硬件厂商如何轻松实现自己的 Codegen,并将其注册为 Relay 后端编译器,从而支持自己的硬件设备/库。本手册涵盖了两种基于不同计算图的 codegen: + +**1. 希望生成 C 代码。** + +如果你的硬件已经具备了一个高度优化的 C/C++ 库,如对于 CPU 而言的 Intel CBLAS/MKL 库,或针对 GPU 而言的 NVIDIA CUBLAS 库,那么本节内容非常适合你。幸运的是,C 源代码模块与 TVM runtime 模块完全兼容,这意味着生成的代码可以由任何具有适当编译标志的 C/C++ 编译器编译,因此用户只需实现一个能为子图生成 C 代码的 codegen,并将 C 源代码模块集成到 TVM runtime 模块中。下一节内容讲详细演示如何为硬件实现 C codegen。 + +**2. 希望生成任意计算图。** + +有时候,硬件可能需要其他形式的计算图如 JSON。这种情况下,用户不仅要实现一个 codegen,还要实现一个自定义 TVM runtime 模块,从而使得 TVM runtime 知道如何执行这个计算图。如果你的硬件已经拥有完整的计算图执行引擎(graph execution engine),如适用于 GPU 的 TensorRT,那么该解决方案对你而言非常具有参考价值。 + +完成 codegen 和 runtime 后,可以让客户借助你的自定义标签,对模型进行注释并加以利用。终端用户如何注释和启动特定 codegen 的教程,将在后续进行补充。 + +# 实现 C Codegen + +在这一部分中,我们将演示如何借助预实现的算子函数,生成 C 代码的 codegen。简单起见,本示例 codegen 不依赖于第三方库。相反,我们在 C 中手动实现了两个宏: + +``` c +#define CSOURCE_BINARY_OP_1D(p_ID_, p_OP_, p_DIM1_) \ + extern "C" void p_ID_(float* a, float* b, float* out) { \ + for (int64_t i = 0; i < p_DIM1_; ++i) { \ + out[i] = a[i] p_OP_ b[i]; \ + } \ + } + +#define CSOURCE_BINARY_OP_2D(p_ID_, p_OP_, p_DIM1_, p_DIM2_) \ + extern "C" void p_ID_(float* a, float* b, float* out) { \ + for (int64_t i = 0; i < p_DIM1_; ++i) { \ + for (int64_t j = 0; j < p_DIM2_; ++j) { \ + int64_t k = i * p_DIM2_ + j; \ + out[k] = a[k] p_OP_ b[k]; \ + } \ + } \ + } +``` + +使用这两个宏,可以为一维和二维张量生成二元算子(binary operator)。例如,给定如下所示的子图,假设所有输入都是 shape 为(10, 10)的二维张量: + +``` text +c_compiler_input0 + | + add <-- c_compiler_input1 + | + subtract <-- c_compiler_input2 + | + multiply <-- c_compiler_input3 + | + out +``` + +我们的目标是生成以下可编译代码来执行子图: + +``` cpp +#include +#include +#include +#include +#include +#include + +#define GCC_BINARY_OP_1D(p_ID_, p_OP_, p_DIM1_) \ + extern "C" void p_ID_(float* a, float* b, float* out) { \ + for (int64_t i = 0; i < p_DIM1_; ++i) { \ + out[i] = a[i] p_OP_ b[i]; \ + } \ + } + +#define GCC_BINARY_OP_2D(p_ID_, p_OP_, p_DIM1_, p_DIM2_) \ + extern "C" void p_ID_(float* a, float* b, float* out) { \ + for (int64_t i = 0; i < p_DIM1_; ++i) { \ + for (int64_t j = 0; j < p_DIM2_; ++j) { \ + int64_t k = i * p_DIM2_ + j; \ + out[k] = a[k] p_OP_ b[k]; \ + } \ + } \ + } + +// 注 1 +GCC_BINARY_OP_2D(gcc_0_0, *, 10, 10); +GCC_BINARY_OP_2D(gcc_0_1, -, 10, 10); +GCC_BINARY_OP_2D(gcc_0_2, +, 10, 10); + +// 注 2 +extern "C" void gcc_0_(float* gcc_input0, float* gcc_input1, + float* gcc_input2, float* gcc_input3, float* out) { + float* buf_0 = (float*)malloc(4 * 100); + float* buf_1 = (float*)malloc(4 * 100); + gcc_0_2(gcc_input0, gcc_input1, buf_0); + gcc_0_1(buf_0, gcc_input2, buf_1); + gcc_0_0(buf_1, gcc_input3, out); + free(buf_0); + free(buf_1); +} + +// 注 3 +extern "C" int gcc_0_wrapper(DLTensor* arg0, DLTensor* arg1, DLTensor* arg2, + DLTensor* arg3, DLTensor* out) { + gcc_0_(static_cast(arg0->data), static_cast(arg1->data), + static_cast(arg2->data), static_cast(arg3->data), + static_cast(out->data)); + return 0; +} +TVM_DLL_EXPORT_TYPED_FUNC(gcc_0, gcc_0_wrapper); +``` + +这里详细介绍一下上面代码里的注释: + +* **注1**:子图中三个节点的函数实现。 +* **注2**:通过分配中间数组(intermediate buffer)并调用相应函数来执行子图的函数。 +* **注3**:TVM runtime 兼容的包装函数。它接收一个输入张量列表和一个输出张量(最后一个参数),并将其转换为正确的数据类型,调用注2 中描述的子图函数。此外,`TVM_DLL_EXPORT_TYPED_FUNC` 是一个 TVM 宏,它通过将所有张量打包到 `TVMArgs` 来生成另一个函数 `gcc_0`,该函数具有统一的函数参数。因此,TVM runtime 可以直接调用 `gcc_0` 来执行子图,无需其他操作。生成上述代码后,TVM 能够将其与计算图的其余部分一起编译并导出单个库以进行部署。 + +在本节的其余部分,我们将逐步创建一个 codegen,来实现上述代码。你的 codegen 必须位于 `src/relay/backend/contrib//`。在这个例子中,我们将 codegen 命名为 "codegen_c",并将其放在 [/src/relay/backend/contrib/codegen_c/](https://github.com/apache/tvm/blob/main/src/relay/backend/contrib/codegen_c/codegen.cc) 目录下。你可以随时查看这个文件,了解完整的实现过程。 + +具体来说,我们将在这个文件中实现两个类,两个类的关系如下: + +``` text + subgraph subgraph +TVM backend -----------------------------> CSourceCodegen -------------> CodegenC + ^ | ^ | + | | | | + ---------------------------------------- ------------------------ + generated C source runtime module generated C code +``` + +当 TVM 后端发现 Relay 计算图中的函数(子图),用注册的编译器标签(本例中为 `ccompiler`)进行了注释时,TVM 后端就会调用 `CSourceCodegen` 并传递子图。 `CSourceCodegen` 的成员函数 `CreateCSourceModule` 将: + +1)为子图生成 C 代码; + +2)将生成的 C 代码包装到 C source runtime 模块中,以便 TVM 后端进行编译和部署。 + +特别是,C codegen 对 `CodegenC` 类是透明的,因为它提供了许多有用的实用程序来简化 codegen 实现。下面的章节将自下而上实现这两个类。 + +## 实现 CodegenC + +在 `src/relay/backend/contrib/codegen_c/codegen.cc` 中,首先在 `tvm.relay.contrib` 的命名空间下创建一个 codegen 类骨架: + +``` cpp +#include +#include +#include +#include +#include + +#include +#include + +#include "codegen_c.h" + +namespace tvm { +namespace relay { +namespace contrib { + +class CodegenC : public ExprVisitor, public CodegenCBase { + public: + explicit CodegenC(const std::string& id) { this->ext_func_id_ = id; } + + void VisitExpr_(const VarNode* node) { ; } + void VisitExpr_(const CallNode* call) final { ; } + std::string JIT() { ; } + + private: + /*! \brief The function id that represents a C source function. */ + std::string ext_func_id_ = ""; + /*! \brief The index of a wrapped C function. */ + int func_idx = 0; + /*! \brief The index of allocated buffers. */ + int buf_idx_ = 0; + /*! \brief The arguments of a C compiler compatible function. */ + std::vector ext_func_args_; + /*! \brief The statements of a C compiler compatible function. */ + std::vector ext_func_body; + /*! \brief The declaration statements of a C compiler compatible function. */ + std::vector func_decl_; + /*! \brief The declaration statements of buffers. */ + std::vector buf_decl_; + /*! \brief The name and index pairs for output. */ + std::vector> out_; +} +``` + +`CodegenC` 类继承了两个类: `ExprVisitor` 提供遍历子图的能力,然后收集所需的信息并生成子图函数,例如 `gcc_0_`。 + +`CodegenCBase` 提供了生成包装函数的能力和实用程序,例如上例中的 `gcc_0`。可以看出,我们只需要在这个 codegen 类中实现三个函数就可以了。 + +### 算子的代码生成 + +首先实现 `VisitExpr_(const CallNode* call)`。该函数在遍历子图时会访问所有调用节点。每个调用节点都包含一个我们想要卸载(offload)到硬件中的算子。因此,我们需要按照拓扑顺序生成具有正确算子的相应 C 代码。完整实现过程如下: + +#### 1. 生成函数声明 + +示例结果:`GCC_BINARY_OP_2D(gcc_0_0, *, 10, 10);` + +要生成函数声明,如上所示,我们需要: + +1)函数名(例如 `gcc_0_0`) + +2)算子的类型(例如 `*` ) + +3)输入张量 shape(例如 `(10, 10)` ) + +这些信息可以从 `CallNode` 轻松获取: + +``` cpp +std::ostringstream macro_stream; +std::ostringstream decl_stream; +std::ostringstream buf_stream; + +// Generate a unique function name you like. +std::string func_name = ext_func_id_ + "_" + std::to_string(func_idx++); + +// Make function declaration string. +macro_stream << "CSOURCE_BINARY_OP_" << call->args.size() << "D(" << func_name << ", "; + +// Check the operator type. +if (IsOp(call, "add")) { + macro_stream << "+"; +} else if (IsOp(call, "subtract")) { + macro_stream << "-"; +} else if (IsOp(call, "multiply")) { + macro_stream << "*"; +} else { + LOG(FATAL) << "Unrecognized op"; +} + +// Extract the input tensor shape. +auto in_shape = GetShape(call->args[0]->checked_type()); +for (size_t i = 0; i < in_shape.size(); ++i) { + macro_stream << ", " << in_shape[i]; +} +macro_stream << ");"; +func_decl_.push_back(macro_stream.str()); +``` + +可以看出,我们将生成的代码推送到类成员变量 `func_decl_` 中。这意味着在我们完成遍历整个子图之后,我们已经收集了所有必需的函数声明,我们唯一需要做的就是用 GCC 编译它们。 `VisitExpr_(const CallNode* call)` 的其余实现也遵循这个概念。 + +#### 2. 生成函数调用 + +示例结果:`gcc_0_0(buf_1, gcc_input3, out);` + +生成函数声明后,我们需要生成一个具有正确输入和输出的函数调用。要想知道调用这个函数时应该放置哪些输入或数组,必须访问它的参数: + +``` cpp +bool first = true; +decl_stream << func_name << "("; +for (size_t i = 0; i < call->args.size(); ++i) { + VisitExpr(call->args[i]); // 注 1 + for (auto out : out_) { + if (!first) { + decl_stream << ", "; + } + first = false; + decl_stream << out.first; + } +} +// 注 2 +``` + +同样,重点介绍一下上述代码中的注释: + +**注1**:`VisitExpr(call->args[i])` 是访问当前函数参数的递归调用。参数可以是另一个节点的输出或输入张量。在该示例中,需要确保每个节点在离开访问器之前,都更新一个类变量 `out_`。图解如下: + +``` text + arg_node arg_node <- Visit arg (Note 1) arg_node + | | | + curr_node <- Process curr_node curr_node <- Put "buf_0" as an input buffer + +(a) out_ = {} (b) out_ = {} (c) out_ = {("buf_0", 20)} +``` + +从上图中可以看出,类变量 `out_` 在访问参数节点前是空的,它被填充了 `arg_node` 输出数组的名称和大小。因此在完成对参数节点的访问时,可以通过查看 `out_` 得知应该放置的正确输入数组。本节末尾以及下一节中,我们将介绍如何更新 `out_`。 + +**注2**:你可能注意到,我们在这一步没有关闭函数调用字符串。当前函数调用字符串看起来像:`gcc_0_0(buf_1, gcc_input3`。这是因为我们没有将最后一个参数(如 output)放入此调用中。函数调用的输出可以是分配的临时数组或子图输出张量。简单起见,在本例中我们为每个调用节点都分配老一个输出数组(下一步),并将最后一个数组中的结果复制到了输出张量。 + +#### 3. 生成输出数组(output buffer) + +示例结果:`float buf_0 = (float)malloc(4 * 100);` + +如上一步所述,除了子图输入和输出张量外,还需要数组来保存中间结果。为了生成数组,我们提取 shape 信息,以确定数组的类型和大小: + +``` cpp +// 这个例子仅支持单个输出。 +auto type_node = call->checked_type().as(); +ICHECK(type_node != nullptr && runtime::TypeMatch(type_node->dtype, kDLFloat, 32)) + << "Only support single output tensor with float type"; + +// 生成一个唯一的数组名字。 +std::string out = "buf_" + std::to_string(buf_idx_++); + +// 提取 shape 作为数组大小。 +auto out_shape = GetShape(call->checked_type()); +int out_size = 1; +for (size_t i = 0; i < out_shape.size(); ++i) { + out_size *= out_shape[i]; +} + +// 分配数组并推送至数组声明 +buf_stream << "float* " << out << " = (float*)std::malloc(4 * " << out_size << ");"; +buf_decl_.push_back(buf_stream.str()); +``` + +分配了输出数组之后,现在可以关闭函数调用字符串,并将生成的函数调用推送到类变量 `ext_func_body`。 + +```plain +decl_stream << ", " << out << ");"; +ext_func_body.push_back(decl_stream.str()); +``` + +#### 4. 更新输出数组 + +为了使得下一个节点(接受当前调用节点的输出,作为其输入)知道它应该使用哪个数组,我们需要在离开这个访问函数之前更新类变量 `out_`: + +```plain +out_.clear(); +out_.push_back({out, out_size}); +``` + +恭喜!到这一步我们已经完成了这个类中最困难的函数。接下来的两节中,我们将进一步完善这个函数的功能。 + +### 输入变量的代码生成 + +回想一下,我们通过访问调用节点的参数(上一节中的第 2 步)收集了输入数组信息,并处理了参数是另一个调用节点的情况(第 4 步)。本节我们将以 `VarNode` 为例,演示如何处理其他节点。 + +`VarNode` 表示模型中的输入张量。它非常重要的一点就是名称提示(例如,`data`、`weight` 等)。访问 `VarNode` 时,只需更新类变量 `out_` 传递名称提示,后代(descendant)调用节点就可以生成正确的函数调用。 + +``` cpp +void VisitExpr_(const VarNode* node) { + ext_func_args_.push_back(node->name_hint()); + out_.clear(); + out_.push_back({node->name_hint(), 0}); +} +``` + +注意:在这个例子中,我们假设要卸载的子图只有调用节点和变量节点。如果子图包含其他类型的节点,如 `TupleNode`,那么你也需要访问它们并绕过输出数组信息。 + +### Code Emitting + +Codegen Class 的最后一部分是 `JIT` 函数,它为子图 emit 一个 C 函数,并将刚生成的 C 代码作为函数体。注意,除了在前几节中生成的子图函数外,还需要一个具有统一参数的 wrapper 函数,供 TVM runtime 调用和传递数据。幸运的是,我们继承的基类已经提供了一个实现,即 `JitImpl`,来生成该函数。调用 `JitImpl`的方式如下: + +``` cpp +JitImpl("gcc_0" /* Subgraph symbol (ID) */, + {"gcc_input0", "gcc_input1", "gcc_input2", "gcc_input3"} /* Input arguments */, + {"float *buf_0 = (float*)malloc(4 * 20)", ...} /* Buffer allocations */, + {"gcc_0_2(gcc_input0, gcc_input1, buf_0);"} /* Function body */, + {"out"} /* Output */); +``` + +上述调用将生成三个函数(一个来自 TVM wrapper 宏): + +1. 子图函数 `gcc_0_`(函数名末尾多了一个下划线)以及为执行子图而生成的所有 C 代码; +2. 带有 `DLTensor` 参数列表的 wrapper 函数 `gcc_0__wrapper_` ,将数据转换为正确的类型并调用 `gcc_0_` +3. TVM runtime 兼容函数 `gcc_0` 具有 TVM 统一函数参数,可解包 TVM 打包张量并调用 `gcc_0__wrapper_` + +因此,在 `JIT` 实现中唯一要做的,就是将生成的所有子图函数代码传递给 `JitImpl`: + +``` cpp +std::string JIT() { + // Write function macros + for (auto decl : func_decl_) { + code_stream_ << decl << "\n"; + } + return JitImpl(ext_func_id_, ext_func_args_, buf_decl_, ext_func_body, out_); +} +``` + +传递的所有变量(`ext_func_id` 等)都是类变量,并在遍历子图时被填充。 + +#### 实现 CSourceCodegen + +创建一个类并实现所需功能,注意:需要继承自 `CSourceModuleCodegenBase`: + +``` cpp +class CSourceCodegen : public CSourceModuleCodegenBase { + public: + // 传递一个子图函数, 并生成 C 代码。 + void GenCFunc(const Function& func) { ; } + + // 使用 GenCFunc 来生成 C 代码并将它包装成一个 C 源模块。 + runtime::Module CreateCSourceModule(const NodeRef& ref) override { ; } + + private: + std::ostringstream code_stream_; +}; +``` + +#### 实现 GenCFunc + +`GenCFunc` 只是简单地使用我们刚刚实现的 `CodegenC` 来遍历一个 Relay 函数(子图),得到生成的 C 代码。内置函数 `GetExtSymbol` 在 Relay 函数中检索唯一的符号名称(例如 `gcc_0`),注意:**必须**将其用作 C 函数名称,因为该符号将用于 DSO 运行查找。 + +```plain +void GenCFunc(const Function& func) { + ICHECK(func.defined()) << "Input error: expect a Relay function."; + + // 记录运行查找的外部符号。 + auto sid = GetExtSymbol(func); + + CodeGenC builder(sid); + builder.VisitExpr(func->body); + code_stream_ << builder.JIT(); +} +``` + +#### 实现 CreateCSourceModule + +此函数为外部库创建了一个 runtime 模块。本事例中,我们创建了一个可以直接被编译并与 TVM 生成的 DSOModule 链接在一起的 CSourceModule。`CodegenC` 实现之后,再实现这个功能就比较简单了: + +``` cpp +runtime::Module CreateCSourceModule(const NodeRef& ref) override { + // 创建头文件 + code_stream_ << "#include \n"; + code_stream_ << "#include \n"; + code_stream_ << "#include \n"; + code_stream_ << "#include \n"; + code_stream_ << "#include \n"; + code_stream_ << "#include \n"; + code_stream_ << "#include \n"; + + // 为算子定义添加一些公共宏。 + const char* operator_macro = R"op_macro( + #define CSOURCE_BINARY_OP_1D(p_ID_, p_OP_, p_DIM1_) \ + extern "C" void p_ID_(float* a, float* b, float* out) { \ + for (int64_t i = 0; i < p_DIM1_; ++i) { \ + out[i] = a[i] p_OP_ b[i]; \ + } \ + } + + #define CSOURCE_BINARY_OP_2D(p_ID_, p_OP_, p_DIM1_, p_DIM2_) \ + extern "C" void p_ID_(float* a, float* b, float* out) { \ + for (int64_t i = 0; i < p_DIM1_; ++i) { \ + for (int64_t j = 0; j < p_DIM2_; ++j) { \ + int64_t k = i * p_DIM2_ + j; \ + out[k] = a[k] p_OP_ b[k]; \ + } \ + } \ + } + )op_macro"; + + code_stream_ << operator_macro << "\n\n"; + + // 为子图生成 C 代码。 + if (ref->IsInstance()) { + GenCFunc(Downcast(ref)); + } else if (ref->IsInstance()) { + relay::Module mod = Downcast(ref); + for (const auto& it : mod->functions) { + GenCFunc(Downcast(it.second)); + } + } else { + LOG(FATAL) << "The input ref is expected to be a Relay function or module" + << "\n"; + } + + // 创建一个 CSourceModule + const auto* pf = runtime::Registry::Get("module.csource_module_create"); + ICHECK(pf != nullptr) << "Cannot find csource module to create the external runtime module"; + return (*pf)(code_stream_.str(), "cc"); +} +``` + +### 注册 CodegenC + +最后一步是将 codegen 注册到 TVM 后端。首先实现一个简单的函数,调用 codegen 并生成一个 runtime 模块: + +``` cpp +runtime::Module CCompiler(const NodeRef& ref) { + CSourceCodegen csource; + return csource.CreateCSourceModule(ref); +} +``` + +接下来将此函数注册到 TVM 后端: + +``` cpp +TVM_REGISTER_GLOBAL("relay.ext.ccompiler").set_body_typed(CCompiler); +``` + +其中 `ccompiler` 是一个自定义标签,它告知 TVM 这是用 `ccompiler` 注释子图时,应该用来生成和卸载子图的 codegen。 + +最后,设置一个 CMake 配置标志,只包含客户的编译器。首先创建一个 cmake 文件:`cmake/modules/contrib/CODEGENC.cmake`: + +``` cpp +if(USE_CODEGENC) + file(GLOB CSOURCE_RELAY_CONTRIB_SRC src/relay/backend/contrib/codegen_c/codegen.cc) + list(APPEND COMPILER_SRCS ${CSOURCE_RELAY_CONTRIB_SRC}) +endif(USE_CODEGENC) +``` + +用户在使用 `config.cmake` 配置 TVM 时,可以自行决定是否配置编译器: + +``` cmake +set(USE_CODEGENC ON) +``` + +## 为表征(Representation)实现 Codegen + +尽管我们已经演示了如何实现 C codegen,但用户硬件可能还需要其他形式的计算图表征(Graph Representation),如 JSON。在这种情况下,用户可以通过修改 `CodegenC` 类,生成自己的计算图表征,并实现一个自定义 runtime 模块,告诉 TVM runtime 如何执行这个计算图表征。 + +简单起见,本指南中定义了一个名为 "ExampleJSON" 的计算图表征。 ExampleJSON 并不是 JSON,而是没有控制流的计算图的简单表示。例如,假设有以下名为 `subgraph_0` 的子图: + +``` text +input0 + | + add <-- input1 + | +subtract <-- input2 + | +multiply <-- input3 + | + out +``` + +那么这个子图的 ExampleJON 看起来类似: + +``` text +subgraph_0 + input 0 10 10 + input 1 10 10 + input 2 10 10 + input 3 10 10 + add 4 inputs: 0 1 shape: 10 10 + sub 5 inputs: 4 2 shape: 10 10 + mul 6 inputs: 5 3 shape: 10 10 +``` + +`input` 关键字声明一个输入张量及其 ID 和 shape;其他语句用 ` inputs: [input ID] shape: [shape]` 语法描述了其计算过程。 + +在本节中,我们试图实现以下自定义 TVM runtime 模块,来执行 ExampleJSON 计算图。 + +``` cpp +runtime::Module ExampleJsonCompiler(const NodeRef& ref) { + ExampleJsonCodeGen codegen(ref); + std::string code = codegen.gen(); // 注 1 + const auto* pf = runtime::Registry::Get("module.examplejson_module_create"); // 注 2 + ICHECK(pf != nullptr) << "Cannot find ExampleJson module to create the external runtime module"; + return (*pf)(code); +} +TVM_REGISTER_GLOBAL("relay.ext.examplejsoncompiler").set_body_typed(ExampleJsonCompiler); +``` + +**注1**:稍后我们将实现一个自定义 codegen,通过取一个子图来生成一个 ExampleJSON 代码字符串。 + +**注2**:此行获取了一个用于创建自定义 runtime 模块的函数的指针。可以看到它采用刚刚生成的 ExampleJSON 格式的子图代码,并对一个 runtime 模块进行了初始化。 + +后续章节中,我们将介绍 1)如何实现 `ExampleJsonCodeGen` 和 2)如何实现和注册 `examplejson_module_create`。 + +### 实现 ExampleJsonCodeGen + +与 C codegen 类似,从 `ExprVisitor` 派生 `ExampleJsonCodeGen` 以访问器模式进行子图遍历。另一方面,因为不会用到 TVM C++ wrapper,所以不必继承 `CodegenCBase`。 codegen 类实现如下: + +``` c++ +#include +#include +#include +#include +#include + +#include +#include + +namespace tvm { +namespace relay { +namespace contrib { + +class ExampleJsonCodeGen : public ExprVisitor { + public: + explicit ExampleJsonCodeGen(); + + // 注 1 + void VisitExpr_(const VarNode* node) { /* Skip in this example. */ } + void VisitExpr_(const CallNode* call) final { /* Skip in this example. */ } + + // 注 2 + std::string gen(NodeRef& ref) { + this->code = ""; + if (ref->IsInstance()) { + this->visit(Downcast(ref)); + } else if (ref->IsInstance()) { + relay::Module mod = Downcast(ref); + for (const auto& it : mod->functions) { + this->visit(Downcast(it.second)); + } + } else { + LOG(FATAL) << "The input ref is expected to be a Relay function or module"; + } + return this->code; + } + + private: + /*! \brief The function id that represents a C source function. */ + std::string code; +} +``` + +**注1**:再次实现相应的 visitor 函数,以生成 ExampleJSON 代码,并将其存储到类变量 `code` 中(由于与 C codegen 基本一致,这里跳过了 visitor 函数的实现)。完成计算图访问后,在 `code` 中会生成一个 ExampleJSON 计算图。 + +**注2**:定义内部 API `gen` 来获取子图,并生成 ExampleJSON 代码。用户可以依据个人喜好,为这个 API 命名。 + +接下来,实现一个自定义 runtime,来利用 `ExampleJsonCodeGen` 的输出。 + +### 实现自定义 runtime + +本节将逐步演示如何自定义 TVM runtime,并将其注册到 TVM runtime 模块。自定义 runtime 应位于 `src/runtime/contrib//`。本示例中,我们将 runtime 命名为 "example_ext_runtime"。 + +首先,如下所示定义一个自定义 runtime 类。注意:这个类必须由 TVM `ModuleNode` 派生,以保证与其他 TVM runtime 模块兼容。 + +``` c++ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace tvm { +namespace runtime { +class ExampleJsonModule : public ModuleNode { + public: + explicit ExampleJsonModule(std::string graph_json); + + PackedFunc GetFunction(const std::string& name, + const ObjectPtr& sptr_to_self) final; + + const char* type_key() const { return "examplejson"; } + + void SaveToBinary(dmlc::Stream* stream) final; + + static Module LoadFromBinary(void* strm); + + static Module Create(const std::string& path); + + std::string GetSource(const std::string& format = ""); + + void Run(int id, const std::vector& inputs, int output); + + void ParseJson(const std::string& json); + + private: + /* \brief 代表计算图的 json 字符串。 */ + std::string graph_json_; + /* \brief 正在被处理的子图。 */ + std::string curr_subgraph_; + /*! \brief 由子图 id 到节点条目的简单图。 */ + std::map > graph_; + /* \brief 包含图中每一个节点的张量的简单池。 */ + std::vector data_entry_; + /* \brief 从节点 id 到算子名字的映射。 */ + std::vector op_id_; +}; +``` + +以下这些从 `ModuleNode` 派生的函数,必须在 `ExampleJsonModule` 中实现: + +* 构造函数:这个类的构造函数,应该接收一个表征中的子图,用户可以自行决定处理和存储的格式。保存的子图可以被以下两个函数使用。 +* `GetFunction`:这是这个类中最重要的函数。当 TVM runtime 要使用编译器标签(compiler tag)执行子图时,它会从自定义 runtime 模块中调用此函数。它提供函数名及 runtime 参数,`GetFunction` 会返回一个打包的函数实现,以供 TVM runtime 执行。 +* `SaveToBinary` 和 `LoadFromBinary`:`SaveToBinary` 将 runtime 模块序列化为二进制格式以供后续部署。用户使用 `export_library` API 时,TVM 会调用这个函数。另一方面,由于用户这时使用的是自己的计算图表征,因此必须确保 `LoadFromBinary` 能够采用`SaveToBinary` 生成的序列化二进制文件,来构造相同的 runtime 模块。 +* `GetSource`(可选):如果想查看生成的 ExampleJSON 代码,可以实现这个函数来转存;否则则可以跳过实现。 + +#### 实现构造函数 + +``` c++ +explicit ExampleJsonModule(std::string graph_json) { + this->graph_json_ = graph_json; + ParseJson(this->graph_json_); +} +``` + +接下来,实现 `ParseJson` 来解析 ExampleJSON 格式的子图,并在内存中构造一个计算图供后续使用。由于本示例不支持带有分支的子图,因此只需用一个数组,按顺序存储子图中的每个节点。 + +``` c++ +void ParseJson(const std::string& json) { + std::string line; + std::string curr_subgraph; + std::stringstream ss(json); + + while (std::getline(ss, line, '\n')) { + std::stringstream ss2(line); + std::string token; + int id = 0; + + ss2 >> token; + if (token.find("subgraph_") != std::string::npos) { + curr_subgraph = token; + continue; + } + + ss2 >> id; + if (op_id_.size() <= static_cast(id)) { + op_id_.resize(id + 1); + data_entry_.resize(id + 1); + } + + int64_t total_elements = 1; + std::vector shape; + if (token == "input") { + int64_t size = 0; + while (ss2 >> size) { + total_elements *= size; + shape.push_back(size); + } + } else { + op_id_[id] = token; // 注 1 + bool shape_data = false; + NodeEntry entry; + while (ss2 >> token) { + if (token == "shape:") { + shape_data = true; + } else if (shape_data) { + total_elements *= std::stoll(token); + shape.push_back(std::stoll(token)); + } else if (token != "inputs:") { + entry.inputs.push_back(std::stoi(token)); + } + } + entry.id = id; + entry.output = id; + graph_[curr_subgraph].push_back(entry); // 注 2 + } + DLDevice dev; + dev.device_type = static_cast(1); + dev.device_id = 0; + data_entry_[id] = NDArray::Empty(shape, DLDataType{kDLFloat, 32, 1}, dev); // 注 3 + } +} +``` + +**注1**:使用类变量 `op_id_` 将子图节点 ID 映射到算子名称(例如 `add`),以便在 runtime 中调用相应的算子函数。 + +**注2**:使用类变量 `graph_` 从子图名称映射到节点数组。`GetFunction` 将在 runtime 通过子图 ID 查询计算图节点。 + +**注3**:使用类变量 *data_entry_* 将子图节点 ID 映射到张量数据占位符。将输入和输出放入 runtime 中对应的数据条目中。 + +#### 实现 GetFunction + +构造函数实现后,以上类变量准备就绪。接下来实现 `GetFunction` 为 TVM runtime 提供可执行的子图函数: + +``` c++ +PackedFunc GetFunction(const std::string& name, + const ObjectPtr& sptr_to_self) final { + if (this->graph_.find(name) != this->graph_.end()) { + this->curr_subgraph_ = name; + return PackedFunc([sptr_to_self, this](TVMArgs args, TVMRetValue* rv) { + + // Copy input tensors to corresponding data entries. + for (auto i = 0; i < args.size(); ++i) { + ICHECK(args[i].type_code() == kNDArrayContainer || args[i].type_code() == kArrayHandle) + << "Expect NDArray or DLTensor as inputs\n"; + if (args[i].type_code() == kArrayHandle) { + DLTensor* arg = args[i]; + this->data_entry_[i].CopyFrom(arg); + } else { + NDArray arg = args[i]; + this->data_entry_[i].CopyFrom(arg); + } + } + + // Execute the subgraph. + for (const auto& it : this->graph_[this->curr_subgraph_]) { + this->Run(it.id, it.inputs, it.output); + } + ICHECK_GT(graph_.count(this->curr_subgraph_), 0U); + + // Copy the output from a data entry back to TVM runtime argument. + auto out_idx = graph_[this->curr_subgraph_].back().output; + if (args[args.size() - 1].type_code() == kArrayHandle) { + DLTensor* arg = args[args.size() - 1]; + this->data_entry_[out_idx].CopyTo(arg); + } else { + NDArray arg = args[args.size() - 1]; + this->data_entry_[out_idx].CopyTo(arg); + } + *rv = data_entry_.back(); + }); + } else { + LOG(FATAL) << "Unknown subgraph: " << name << "\n"; + return PackedFunc(); + } +} +``` + +可以看出,`GetFunction` 由三个主要部分组成。第一部分将数据从 TVM runtime 参数,复制到构造函数中指定的对应数据条目。第二部分使用 `Run` 函数执行子图(并稍后实现),并将结果保存到另一个数据条目。第三部分将输出数据条目中的结果,复制回对应的 TVM runtime 参数进行输出。 + +#### 实现 Run + +`Run` 函数接收 1)子图 ID,2)输入数据条目索引列表和 3)输出数据条目索引。 + +``` c++ +void Run(int id, const std::vector& inputs, int output) { + // Make a list data entry indexs. + std::vector args(inputs.begin(), inputs.end()); + args.push_back(output); + + // Initialize data holders. + std::vector values(args.size()); + std::vector type_codes(args.size()); + + // Initialize a TVM arg setter with TVMValue and its type code. + TVMArgsSetter setter(values.data(), type_codes.data()); + + // Set each argument to its corresponding data entry. + if (op_id_[id] == "add" || op_id_[id] == "sub" || op_id_[id] == "mul") { + for (size_t i = 0; i < args.size(); i++) { + setter(i, data_entry_[args[i]]); + } + } + + // Invoke the corresponding operator function. + if (op_id_[id] == "add") { + Add(values.data(), type_codes.data(), args.size()); + } else if (op_id_[id] == "sub") { + Sub(values.data(), type_codes.data(), args.size()); + } else if (op_id_[id] == "mul") { + Mul(values.data(), type_codes.data(), args.size()); + } else { + LOG(FATAL) << "Unknown op: " << op_id_[id] << "\n"; + } +} +``` + +`Run` 函数主要包括两部分。第一部分负责分配 `TVMValue` 列表,并映射相应的数据输入块。这也会成为算子函数的参数。第二部分调用算子函数。尽管使用的 C 函数与上一个示例相同,但用户可以将 `Add`、`Sub` 和 `Mul` 替换为自己的引擎。注意,这里需要确保引擎将结果存储到最后一个参数,从而使得它们可以传输回 TVM runtime。 + +实现上述功能后,用户自定义的 codegen 和 runtime 就可以执行子图了。最后一步是注册一个 API(`examplejson_module_create`)来创建这个模块: + +``` c++ +TVM_REGISTER_GLOBAL("module.examplejson_module_create") +.set_body_typed([](std::string code){ + auto n = make_object(code); + return runtime::Module(n); +}); +``` + +#### 实现 SaveToBinary 和 LoadFromBinary + +到目前为止,我们已经实现了与其他 TVM runtime 用法一致的自定义 runtime 的主要功能。但是,当用户想要将构建的 runtime 保存到磁盘以进行部署时,TVM 不知道如何保存。这就是实现 `SaveToBinary` 和 `LoadFromBinary` 的原因,它们会告诉 TVM 这个自定义 runtime 如何持久化和复原。 + +首先实现 `SaveToBinary` 函数,允许用户将此模块保存在磁盘中。 + +``` c++ +void SaveToBinary(dmlc::Stream* stream) final { + stream->Write(this->graph_json_); +} +``` + +这个函数非常简单。在构造函数中,我们采取的唯一参数是一个子图表征(subgraph representation)。也就是说只需一个子图表征来构造/恢复这个自定义 runtime 模块。`SaveToBinary` 只是将子图写到一个输出的 DMLC 流中,当用户使用 `export_library` API 输出模块时,自定义模块将是一个子图的 ExampleJSON 流。 + +`LoadFromBinary` 读取子图流并重新构建自定义 runtime 模块的流程与此类似: + +``` c++ +static Module LoadFromBinary(void* strm) { + dmlc::Stream* stream = static_cast(strm); + std::string graph_json; + stream->Read(&graph_json); + auto n = tvm::runtime::make_object(graph_json); + return Module(n); +} +``` + +此外,还需要注册以下函数,启用相应的 Python API: + +``` c++ +TVM_REGISTER_GLOBAL("module.loadbinary_examplejson") +.set_body_typed(ExampleJsonModule::LoadFromBinary); +``` + +上述注册意味着当用户调用 `tvm.runtime.load_module(lib_path)` API,并且导出库有一个 ExampleJSON 流时,`LoadFromBinary` 将被调用以创建相同的自定义 runtime 模块。 + +另外,如果想支持直接从 ExampleJSON 文件创建模块,还可以实现一个非常简单的函数,并注册一个 Python API,如下所示: + +``` c++ +static Module Create(const std::string& path) { + std::ifstream filep; + filep.open(path, std::ios::in); + std::string graph_json; + std::string line; + while (std::getline(filep, line)) { + graph_json += line; + graph_json += "\n"; + } + filep.close(); + auto n = tvm::runtime::make_object(graph_json); + return Module(n); +} + +TVM_REGISTER_GLOBAL("module.loadfile_examplejson") +.set_body([](TVMArgs args, TVMRetValue* rv) { + *rv = ExampleJsonModule::Create(args[0]); +}); +``` + +这意味着用户可以手动编写/修改 ExampleJSON 文件,并使用 Python API `tvm.runtime.load_module("mysubgraph.examplejson", "examplejson")` 构建自定义模块。 + +## 总结 + +汇总前文重点: + +* 从 `ExprVisitor` 和 `CodegenCBase`(仅适用于 C codegen)派生的 codegen 类,具有以下功能: + * `VisitExpr_(const CallNode* call)` 收集调用节点信息。 + * 收集子图信息所需的其他 visitor 函数。 + * `JIT` 生成子图代码。 + * 注册 codegen。 +* 创建 `CSourceModule` 的函数(用于 C codegen)。 +* 从 `ModuleNode` 派生的 runtime 模块类,具有以下功能(用于计算图表征)。 + * 构造函数。 + * `GetFunction` 生成与 TVM runtime 兼容的 `PackedFunc`。 + * `Run` 执行子图。 + * 注册 runtime creation API。 + * `SaveToBinary` 和 `LoadFromBinary` 序列化/反序列化自定义 runtime 模块。 + * 注册 `LoadFromBinary` API 为`tvm.runtime.load_module(your_module_lib_path)`提供支持。 + * (可选)`Create` 支持从表征的子图文件,构建自定义 runtime 模块。 +* 一个注释器,用于注释用户 Relay 程序,利用编译器和 runtime(待定)。 diff --git a/versioned_docs/version-0.12.0/dev/how_to/05-python_target_parametrization.md b/versioned_docs/version-0.12.0/dev/how_to/05-python_target_parametrization.md new file mode 100644 index 00000000..78deaddc --- /dev/null +++ b/versioned_docs/version-0.12.0/dev/how_to/05-python_target_parametrization.md @@ -0,0 +1,128 @@ +--- +title: Python Target 参数化 +--- + +# Python Target 参数化 + +## 摘要 + +对于任何支持的 runtime,TVM 都应该输出正确的数字结果。因此,在编写验证数字输出的单元测试时,这些单元测试应该在所有支持的 runtime 上都能正常运行。由于这是一个非常常见的用例,TVM 的辅助函数可以对所有单元测试进行参数化,从而便于单元测试在所有启用并具有兼容设备的 target 上运行。 + +测试套件的单个 Python 函数,可以扩展为几个参数化单元测试,每个单元测试一个 target 设备。为了保证测试正常运行,以下所有条件必须为 True: + +* 测试存在于已经传递给 *pytest* 的文件或目录中。 +* 应用于函数的 pytest 标记,无论是显式还是通过 target 参数化,都必须与传递给 pytest 的 *-m* 参数的表达式兼容。 +* 对于使用 target fixture 的参数化测试,target 必须出现在环境变量 *TVM_TEST_TARGETS* 中。 +* 对于使用 *target* fixture 的参数化测试,config.cmake 中的构建配置必须启用相应的 runtime。 + +## 单元测试文件内容 + +在多个 target 上运行测试,推荐方法是通过参数化测试。对于一个固定的 target 列表,可以通过用 `@tvm.testing.parametrize_targets('target_1', 'target_2', ...)` 修饰同时接受 `target` 或 `dev` 作为函数参数来显式地完成。 + +该函数将为列出的每个 target 都运行一遍,并单独报告每个 target 的运行结果(成功/失败)。如果一个 target 因为在 *config.cmake* 中被禁用而无法运行,或者因为没有合适的硬件存在,那么这个 target 将被报告为跳过。 + +``` python +# 显式列出要使用的 target +@tvm.testing.parametrize_target('llvm', 'cuda') +def test_function(target, dev): + # 测试代码写在这里 +``` + +对于在所有 target 上都能正常运行的测试,可以省略装饰器。任何接收 `target` 或 `dev` 参数的测试,都将自动在 `TVM_TEST_TARGETS` 指定的所有 target 上进行参数化。参数化为每个 target 提供了相同的成功/失败/跳过报告,同时允许轻松扩展测试套件,以覆盖额外的 target。 + +``` python +# 隐式参数化以运行在所有 target 上 +# 在环境变量 TVM_TEST_TARGETS 里 +def test_function(target, dev): + # 测试代码写在这里 +``` + +`@tvm.testing.parametrize_targets` 也可以用作裸装饰器(bare decorator)来显式地进行参数化,但没有额外的效果。 + +``` python +# 隐式参数化以运行在所有 target 上 +# 在环境变量 TVM_TEST_TARGETS 里 +@tvm.testing.parametrize_targets +def test_function(target, dev): + # 测试代码写在这里 +``` + +可以使用 `@tvm.testing.exclude_targets` 或 `@tvm.testing.known_failing_targets` 装饰器,将特定 target 排除或标记为预期失败。更多信息,请参阅文档字符串。 + +在某些情况下,可能需要跨多个参数进行参数化。例如,可能存在一些待测试的 target-specific 实现方法,其中一些 target 的实现方法还不止一个。这可以通过显式地参数化参数元组来完成,如下所示。在这种情况下,只有显式地列出的 target 会运行,但它们仍会应用适当的 `@tvm.testing.requires_RUNTIME` 标记。 + +``` python +pytest.mark.parametrize('target,impl', [ + ('llvm', cpu_implementation), + ('cuda', gpu_implementation_small_batch), + ('cuda', gpu_implementation_large_batch), +]) +def test_function(target, dev, impl): + # 测试代码写在这里 +``` + +参数化功能是在 pytest 标记之上实现的。每个测试函数都可以用 [pytest 标记](#) 装饰以包含元数据。最常用的标记如下: + +* `@pytest.mark.gpu` - 将函数标记为使用 GPU 功能。这本身是没有效果的,但可以与命令行参数 `-m gpu` 或 `-m 'not gpu'` 搭配使用,从而限制 pytest 要执行哪些测试。这不应该单独调用,而应该是单元测试中使用的其他标记的一部分。 +* `@tvm.testing.uses_gpu` - 应用 `@pytest.mark.gpu`。用于标记可能使用 GPU 的单元测试(如果有)。只有在显式循环 `tvm.testing.enabled_targets()` 的测试中,才需要这个装饰器,不过这已经不是编写单元测试的首选方法了(见下文)。使用 `tvm.testing.parametrize_targets()` 时,此装饰器对于 GPU target 是隐式的,不需要显式地应用。 +* `@tvm.testing.requires_gpu` - 应用 `@tvm.testing.uses_gpu`,如果没有 GPU,还要标记这个测试应该被跳过(`@pytest.mark.skipif`)。 +* `@tvfm.testing.requires_RUNTIME` - 几个装饰器(例如 `@tvm.testing.requires_cuda`),如果指定 runtime 不可用,每个装饰器都会跳过测试。runtime 如果在 `config.cmake` 中被禁用,或是不存在兼容设备时,则该 runtime 不可用。对于使用 GPU 的 runtime,包含 `@tvm.testing.requires_gpu`。 + +使用参数化 target 时,每个测试运行都是用跟正在使用的 target 相对应的 `@tvm.testing.requires_RUNTIME` 修饰的。因此,如果某个 target 在 `config.cmake` 中被禁用,或没有合适的硬件可以运行,它将被显式列为跳过。 + +还有 `tvm.testing.enabled_targets()`,根据环境变量 `TVM_TEST_TARGETS`、构建配置和存在的物理硬件,返回所有在当前机器上启用和可运行的 target。大多数当前测试显式循环是 `enabled_targets()` 返回 target,但它无法应用于新测试。这种类型的 pytest 输出会自动跳过在 `config.cmake` 中禁用,或者没有运行设备的 runtime。此外,测试会在第一个失败的 target 上停止,这对于判断错误是发生在某一个还是所有 target 上,都很困难。 + +``` python +# 老的风格, 已弃用。 +def test_function(): + for target,dev in tvm.testing.enabled_targets(): + # 测试代码写在这里 +``` + +## 本地运行 + +要在本地运行 Python 单元测试,可以使用 `${TVM_HOME}` 目录中的命令 `pytest`。 + +* 环境变量 + + * `TVM_TEST_TARGETS` 应该是一个用分号分隔的待运行 target 列表。如果未设置,默认是 `tvm.testing.DEFAULT_TEST_TARGETS` 中定义的 target。 + + 注意:如果 `TVM_TEST_TARGETS` 不包含任何已启用且具有该类型可访问设备的 target,则测试将回退到仅在 `llvm` target 上运行。 + + * `TVM_LIBRARY_PATH` 应该是 `libtvm.so` 库的路径。例如,这可以用来借助调试版本运行测试。如果未设置,将搜索相对于 TVM 源目录的 `libtvm.so`。 + +* 命令行参数 + + * 传递文件夹或文件的路径,将仅在该文件夹或文件中运行单元测试。这一点很实用,例如,避免在未安装特定前端的系统上,运行位于 `tests/python/frontend` 中的测试。 + + * `-m` 参数仅运行带有特定 pytest 标记的单元测试。最常见的用法是使用 `m gpu` 仅运行标有 `@pytest.mark.gpu` 的测试,并使用 GPU 运行。通过传递 `m 'not gpu'`,它也可以用于仅运行不使用 GPU 的测试。 + + 注意:此过滤发生在基于 `TVM_TEST_TARGETS` 环境变量选定 target 之后。即使指定了 `-m gpu`,如果 `TVM_TEST_TARGETS` 不包含 GPU target,也不会运行任何 GPU 测试。 + +## 在本地 Docker 容器中运行 + +与在 CI 中的用法类似,`docker/bash.sh` 脚本可用于在同一 Docker 镜像中运行单元测试。第一个参数应指定要运行的 Docker 镜像(例如 `docker/bash.sh ci_gpu`)。允许的镜像名称在位于 TVM 源目录的 Jenkinsfile 顶部定义,并映射到 [tlcpack](https://hub.docker.com/u/tlcpack) 中的镜像。 + +如果没有给出额外的参数,Docker 镜像将被载入一个交互式 bash 会话。如果脚本作为可选参数传递(例如 `docker/bash.sh ci_gpu tests/scripts/task_python_unittest.sh`),则该脚本将在 Docker 镜像中执行。 + +注意:Docker 镜像包含所有系统依赖项,但不包括这些系统的 `build/config.cmake` 配置文件。 TVM 源目录用作 Docker 镜像的主目录,因此这将默认使用与本地配置相同的 config/build 目录。一种解决方案是单独维护 `build_local` 和 `build_docker` 目录,并在进入/退出 Docker 时,创建从 `build` 到相应文件夹的符号链接。 + +## 在 CI 中运行 + +CI 中的所有内容都从 Jenkinsfile 中的任务定义开始的。这包括定义使用哪个 Docker 镜像,编译时配置是什么,以及哪些阶段都各自包含哪些测试。 + +* Docker 镜像 + + Jenkinsfile 的每个任务(例如 'BUILD: CPU')都会调用 `docker/bash.sh`。调用 docker/bash.sh 后面的参数定义了 CI 中的 Docker 镜像,就本地类似。 + +* Compile-time 配置 + + Docker 镜像没有内置 `config.cmake` 文件,因此这是每个 `BUILD` 任务的第一步。这一步是使用 `tests/scripts/task_config_build_*.sh` 脚本完成的。使用哪个脚本取决于正在测试的构建,还需要在 Jenkinsfile 中指定。 + + 每个 `BUILD` 任务都以打包一个供以后测试使用的库而结束。 + +* 运行哪些测试 + + Jenkinsfile 的 `Unit Test` 和 `Integration Test` 阶段决定了如何调用 `pytest`。每个任务都是先解压一个编译库,这个库在先前的 `BUILD` 阶段已经编译过了。接下来运行测试脚本(如`tests/script/task_python_unittest.sh`)。这些脚本可以设定文件/文件夹,以及传递给 `pytest` 的命令行选项。 + + 其中一些脚本包含 `-m gpu` 选项,该选项将测试限制为仅运行包含 `@pytest.mark.gpu` 标记的测试。 diff --git a/versioned_docs/version-0.12.0/dev/index.md b/versioned_docs/version-0.12.0/dev/index.md new file mode 100644 index 00000000..10552db4 --- /dev/null +++ b/versioned_docs/version-0.12.0/dev/index.md @@ -0,0 +1,7 @@ +--- +title: 开发者教程 +--- + +本章为 TVM 代码库指南,主要介绍开发者如何为项目做贡献。 + +* [TVM 代码库实例讲解](tutorial/codebase_walkthrough) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/dev/tutorial/codebase_walkthrough.md b/versioned_docs/version-0.12.0/dev/tutorial/codebase_walkthrough.md new file mode 100644 index 00000000..5e0e2a74 --- /dev/null +++ b/versioned_docs/version-0.12.0/dev/tutorial/codebase_walkthrough.md @@ -0,0 +1,231 @@ +--- +title: TVM 代码库实例讲解 +--- + +了解新代码库是一个挑战,对于 TVM +这样组件众多、交互方式复杂的代码库来说更是如此。本指南将通过简单示例,介绍构成编译管道的关键部分,以及所有重要步骤在代码库中的实现位置,从而帮助开发者更快速地上手 +TVM。 + +## 代码库结构概述 + +TVM 仓库的根目录,包括以下几个子目录: + +- `src` - 用于算子编译和部署 runtime 的 C++ 代码。 +- `src/relay` - Relay 的实现,一种用于深度学习框架的新功能 IR。 +- `python` - Python 前端,用于包装 `src` 中实现的 C++ 函数和对象。 +- `src/topi` - 标准神经网络算子的计算定义和后端调度。 + +用标准的深度学习术语来解释,`src/relay` +是管理计算图的组件,图结构中的节点使用 `src` +其余部分实现的基础架构进行编译和执行。`python` 为 C++ API +和执行编译的驱动代码,提供 Python 绑定。与节点对应的算子注册在 +`src/relay/op` 中。算子的实现在 `topi` 中,所用编程语言为 C++ 或 +Python。 + +用户通过 `relay.build(...)` +调用图结构编译时,图结构中的所有节点的序列会发生以下变化: + +- 通过查询算子注册表来查找算子的实现 +- 为算子生成计算表达式和调度 +- 将算子编译成目标代码 + +TVM 代码库有趣的地方在于 C++ 和 Python +之间的互操作性不是单向的。通常情况下,所有执行繁重任务的代码都是用 C++ +实现的,而 Python 绑定用于用户界面。TVM 中也是如此,只不过在 TVM +代码库中,C++ 代码也可以调用 Python 模块中定义的函数。例如,卷积算子是在 +Python 中实现的,它的实现是由 Relay 中的 C++ 代码调用的。 + +## 向量加法示例 + +本文档将借助简单示例 \-- 向量加法,介绍如何直接调用底层 TVM +API。关于向量加法的详细介绍,请查看:[使用张量表达式处理算子](/docs/tutorial/tensor_expr) + +``` python +n = 1024 +A = tvm.te.placeholder((n,), name='A') +B = tvm.te.placeholder((n,), name='B') +C = tvm.te.compute(A.shape, lambda i: A[i] + B[i], name="C") +``` + +这里,定义在 `python/tvm/te/tensor.py` 中的 `A`、`B`和 `C`,类型都是 +`tvm.tensor.Tensor`。Python `Tensor` 由 C++ `Tensor` 支持,在 +`include/tvm/te/tensor.h` 和 `src/te/tensor.cc` 中实现。TVM 中的所有 +Python 类型都可以视为具有相同名称的底层 C++ 类型的句柄。查看以下 Python +`Tensor` 类型的定义,可以发现它是 `Object` 的一个子类: + +``` python +@register_object +class Tensor(Object, _expr.ExprOp): + """Tensor object, to construct, see function.Tensor""" + + def __call__(self, *indices): + ... +``` + +对象协议是将 C++ 类型暴露给前端语言(包括 Python)的基础。TVM 实现 +Python 封装的方式并不直接。在 [TVM Runtime 系统](/docs/arch/arch/runtimes) 中简单介绍了这一点,感兴趣的朋友可以在 `python/tvm/_ffi/` +中查看细节。 + +使用 `TVM_REGISTER_*` 宏将 C++ 函数以 [PackedFunc](/docs/arch/arch/runtimes#PackedFunc) 的形式暴露给前端语言。`PackedFunc` 是 TVM 实现 C++ 和 Python +之间互操作性的另一种机制。这使得从 C++ 代码库中调用 Python +函数变得非常容易。Python 和 C++ 的语言交互接口(FFI) +的调用之间导航,请查看 [FFI Navigator](https://github.com/tqchen/ffi-navigator)。 + +每个 `Tensor` 对象有一个与之相关的 `Operation` 对象,定义在 +`python/tvm/te/tensor.py`、`include/tvm/te/operation.h` 和 +`src/tvm/te/operation` 子目录下。`Tensor` 是其 `Operation` +对象的输出。每个 `Operation` 对象都有 `input_tensors()` +方法,该方法返回一个输入 `Tensor` 列表。这样我们就可以跟踪 `Operation` +之间的依赖关系。 + +将输出张量 `C` 对应的 op 传递给 `python/tvm/te/schedule.py` 中的 +`tvm.te.create_schedule()` 函数。 + +``` python +s = tvm.te.create_schedule(C.op) +``` + +这个函数被映射到 `include/tvm/schedule.h` 中的 C++ 函数。 + +``` python +inline Schedule create_schedule(Array ops) { + return Schedule(ops); +} +``` + +`Schedule` 由 `Stage` 和输出 `Operation` 的集合组成。 + +`Stage` 对应一个 `Operation`。在上述 Vector Add 示例中,有两个占位符 op +和一个计算 op,所以调度 s 包含三个阶段。每个 Stage +都有关于循环嵌套结构的信息,每个循环的类型(`Parallel`、`Vectorized`、`Unrolled`),以及在下一个 +`Stage` 的循环嵌套中(如果有的话)执行其计算的位置。 + +`Schedule` 和 `Stage` 在 +`tvm/python/te/schedule.py`、`include/tvm/te/schedule.h` 和 +`src/te/schedule/schedule_ops.cc` 中定义。 + +简单来说,上述 `create_schedule()` 函数创建的默认 schedule 调用 +`tvm.build(...)`。 + +``` python +target = "cuda" +fadd = tvm.build(s, [A, B, C], target) +``` + +定义在 `python/tvm/driver/build_module.py` 中的 `tvm.build()`,接收一个 schedule,输入和输出 `Tensor` 以及一个 target,然后返回一个 [tvm.runtime.Module](https://tvm.apache.org/docs/reference/api/python/runtime.html#tvm.runtime.Module) 对象。一个 [tvm.runtime.Module](https://tvm.apache.org/docs/reference/api/python/runtime.html#tvm.runtime.Module) 对象包含一个可以用函数调用语法来调用的已编译函数。 + +`tvm.build()` 的过程可以分为两个步骤: + +- 降级,高级的、初始的循环嵌套结构被转化为最终的、底层的 IR +- 代码生成,由底层的 IR 来生成目标机器代码 + +降级是由 `tvm.lower()` 函数完成的,定义在 `python/tvm/build_module.py` +中。首先进行边界推断,然后创建一个初始循环嵌套结构。 + +``` python +def lower(sch, + args, + name="default_function", + binds=None, + simple_mode=False): + ... + bounds = schedule.InferBound(sch) + stmt = schedule.ScheduleOps(sch, bounds) + ... +``` + +边界推断(Bound inference)是推断出所有循环边界和中间缓冲区大小的过程。如果你的目标是 CUDA 后端,并且使用了共享内存,那么它所需的最小尺寸就会在这里自动确定。边界推断在 `src/te/schedule/bound.cc`、`src/te/schedule/graph.cc` 和 `src/te/schedule/message_passing.cc` 中实现。更多关于边界推断的信息,请参阅 [InferBound Pass](/docs/arch/arch/inferbound)。 + +`stmt` 是 `ScheduleOps()` 的输出,代表一个初始的循环嵌套结构。如果 schedule 已经应用了 `reorder` 或 `split` 原语,则初始循环嵌套已经反映了这些变化。`ScheduleOps()` 在 `src/te/schedule/schedule_ops.cc` 中定义。 + +接下来,对 `stmt` 在 `src/tir/pass` 子目录下进行降级处理。例如,如果 `vectorize` 或 `unroll` 原语已经应用于 schedule 了,那么它们将被应用于以下步骤: + +``` python +... +stmt = ir_pass.VectorizeLoop(stmt) +... +stmt = ir_pass.UnrollLoop( + stmt, + cfg.auto_unroll_max_step, + cfg.auto_unroll_max_depth, + cfg.auto_unroll_max_extent, + cfg.unroll_explicit) +... +``` + +降级完成后,`build()` 函数从降级的函数中生成目标机器代码。如果目标是 x86,这段代码会包含 SSE 或 AVX 指令;如果目标是 CUDA,则包含 PTX 指令。除了目标专用机器代码外,TVM 还会生成负责内存管理、内核启动等的宿主机代码。 + +代码生成是由 `build_module()` 函数完成的,定义在 `python/tvm/target/codegen.py`。在 C++ 端,代码生成是在 `src/target/codegen` 子目录下实现的。`build_module()` 这个 Python 函数将进入下面 `src/target/codegen/codegen.cc` 中的 `Build()` 函数。 + +`Build()` 函数在 `PackedFunc` 注册表中查找给定目标的代码生成器,并调用找到的函数。例如,`codegen.build_cuda` 函数在 `src/codegen/build_cuda_on.cc` 中注册,如下所示: + +``` c++ +TVM_REGISTER_GLOBAL("codegen.build_cuda") +.set_body([](TVMArgs args, TVMRetValue* rv) { + *rv = BuildCUDA(args[0]); +}); +``` + +上述 `BuildCUDA()` 使用 `src/codegen/codegen_cuda.cc` 中定义的 `CodeGenCUDA` 类从降级的 IR 中生成 CUDA 内核源代码,并使用 NVRTC 编译内核。如果目标是使用 LLVM 的后端,包括 x86、ARM、NVPTX 和 AMDGPU,代码生成主要由定义在 `src/codegen/llvm/codegen_llvm.cc` 中的 `CodeGenLLVM` 类完成。`CodeGenLLVM` 将 TVM IR 翻译成 LLVM IR,运行一些 LLVM 优化,并生成目标机器代码。 + +`src/codegen/codegen.cc` 中的 `Build()` 函数返回一个 `runtime::Module` 对象,该对象在 `include/tvm/runtime/module.h` 和 `src/runtime/module.cc` 中定义。`Module` 对象是底层目标特定的 `ModuleNode` 对象的容器。每个后端都实现了一个 `ModuleNode` 的子类,以添加目标特定 runtime API 调用。例如,CUDA 后端在 `src/runtime/cuda/cuda_module.cc` 中实现了 `CUDAModuleNode` 类,它管理着 CUDA 驱动 API。上述 `BuildCUDA()` 函数用 `runtime::Module` 包装了 `CUDAModuleNode` 并将其返回到 Python 端。LLVM 后端在 `src/codegen/llvm/llvm_module.cc` 中实现了 `LLVMModuleNode`,它负责处理编译代码的 JIT 执行。`ModuleNode` 的其他子类可以在与每个后端对应的 `src/runtime` 的子目录下找到。 + +返回的模块可以被认为是已编译的函数和设备 API 的结合,可以在 TVM 的 +NDArray 对象上被调用。 + +``` python +dev = tvm.device(target, 0) +a = tvm.nd.array(np.random.uniform(size=n).astype(A.dtype), dev) +b = tvm.nd.array(np.random.uniform(size=n).astype(B.dtype), dev) +c = tvm.nd.array(np.zeros(n, dtype=C.dtype), dev) +fadd(a, b, c) +output = c.numpy() +``` + +在底层,TVM 自动分配设备内存并管理内存传输。为此,每个后端都需要继承在 `include/tvm/runtime/device_api.h` 中定义的 DeviceAPI 类,并覆盖内存管理方法以使用特定于设备的 API。例如,CUDA 后端在 `src/runtime/cuda/cuda_device_api.cc` 中实现 `CUDADeviceAPI` 以使用 `cudaMalloc`、`cudaMemcpy` 等。 + +首次使用 `fadd(a, b, c)` 调用已编译的模块时,会调用 `ModuleNode` 的 `GetFunction()` 方法来获取可用于内核调用的 `PackedFunc`。例如,在 `src/runtime/cuda/cuda_module.cc` 中,CUDA 后端实现了 `CUDAModuleNode::GetFunction()`,如下所示: + +``` c++ +PackedFunc CUDAModuleNode::GetFunction( + const std::string& name, + const std::shared_ptr& sptr_to_self) { + auto it = fmap_.find(name); + const FunctionInfo& info = it->second; + CUDAWrappedFunc f; + f.Init(this, sptr_to_self, name, info.arg_types.size(), info.launch_param_tags); + return PackFuncVoidAddr(f, info.arg_types); +} +``` + +`PackedFunc` 的重载 `operator()` 将被调用,进而调用 `src/runtime/cuda/cuda_module.cc` 中 `CUDAWrappedFunc` 的 `operator()`,最后实现 `cuLaunchKernel` 驱动程序的调用: + +``` c++ +class CUDAWrappedFunc { + public: + void Init(...) + ... + void operator()(TVMArgs args, + TVMRetValue* rv, + void** void_args) const { + int device_id; + CUDA_CALL(cudaGetDevice(&device_id)); + if (fcache_[device_id] == nullptr) { + fcache_[device_id] = m_->GetFunc(device_id, func_name_); + } + CUstream strm = static_cast(CUDAThreadEntry::ThreadLocal()->stream); + ThreadWorkLoad wl = launch_param_config_.Extract(args); + CUresult result = cuLaunchKernel( + fcache_[device_id], + wl.grid_dim(0), + wl.grid_dim(1), + wl.grid_dim(2), + wl.block_dim(0), + wl.block_dim(1), + wl.block_dim(2), + 0, strm, void_args, 0); + } +}; +``` + +以上就是 TVM 编译和执行函数的相关简介。虽然没有涉及到 TOPI 或 Relay 的详细介绍,但所有神经网络算子的编译过程都和上述过程类似。欢迎各位开发者深入研究代码库其他部分的细节。 diff --git a/versioned_docs/version-0.12.0/getting_started/_category_.json b/versioned_docs/version-0.12.0/getting_started/_category_.json new file mode 100644 index 00000000..5b6b819a --- /dev/null +++ b/versioned_docs/version-0.12.0/getting_started/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "快速上手", + "position": 100 +} diff --git a/versioned_docs/version-0.12.0/how_to/11-errors.md b/versioned_docs/version-0.12.0/how_to/11-errors.md new file mode 100644 index 00000000..e0112de6 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/11-errors.md @@ -0,0 +1,36 @@ +--- +title: 处理 TVM 报错 +sidebar_position: 210 +--- + +# 处理 TVM 报错 + +运行 TVM 时,可能会遇到如下报错: + +``` plain +--------------------------------------------------------------- +An error occurred during the execution of TVM. +For more information, please see: https://tvm.apache.org/docs/errors.html +--------------------------------------------------------------- +``` + +下面解释了这些错误消息是如何产生的,以及当错误发生时要怎么做。 + +## 这些错误从何而来? + +这个错误是在 TVM 执行期间违反内部不变量引起的。从技术层面来看,消息是 `ICHECK` 宏(位于 `include/tvm/runtime/logging.h` 中)生成的,TVM 多处代码用到 `ICHECK` 宏来断言执行期间某个条件为真;一旦断言失败,TVM 都会退出,并显示如上错误消息。 + +有关 TVM 中错误是如何生成并处理的更多详细信息,参阅*错误处理指南*。 + +## 遇到这种错误怎么办? + +最好的做法是在 [Apache TVM 论坛](https://discuss.tvm.apache.org/) 中搜索遇到的错误,看看其他人是否遇到过,以及可能的解决方案。如果错误已经在 TVM 更新版本中进行了修复,你可以更新 TVM 的版本。 + +若论坛上没有相关帖子,欢迎在论坛上创建一个新帖子描述问题的详细信息。*请在帖子中包含以下关键信息*: + +* 当前 TVM 版本(例如,源代码树的 git commit 的哈希值)。 +* TVM 运行的硬件和操作系统的版本。 +* TVM 编译的硬件设备和操作系统。 +* 用于重现此问题的信息,如模型、输入或其他信息。 + +如果没有这些信息,TVM 开发者很难给予你帮助。 \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/12-FAQ.md b/versioned_docs/version-0.12.0/how_to/12-FAQ.md new file mode 100644 index 00000000..f08874b9 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/12-FAQ.md @@ -0,0 +1,26 @@ +--- +title: FAQ +sidebar_position: 220 +--- + +# FAQ +## 如何安装 +参阅 [安装 TVM](/docs/install) + +## 如何添加新的硬件后端 +* 如果硬件后端支持 LLVM,则可以直接通过在 `target` 中设置正确的 `target` 三元组来生成代码。 +* 如果 target 硬件是 GPU,请用 cuda、opencl 或 vulkan 后端。 +* 如果 target 硬件是一个特殊的加速器,请查看 [VTA:多功能张量加速器](/docs/topic/vta) 和 [向 TVM 中添加自定义 Codegen ](/docs/dev/how_to/relay_bring_your_own_codegen)。 +* 对上述所有情况,若要用 AutoTVM 添加 target-specific 的优化模板,请参阅 [使用模板和 AutoTVM 进行自动调优](/docs/how_to/autotune)。 +* 除了使用 LLVM 的向量化,还可以嵌入微内核来利用硬件内联函数,请参阅 [使用 Tensorize 来利用硬件内联函数](/docs/how_to/te_schedules/tensorize)。 + +## TVM 与其他 IR/DSL 项目的关系 +深度学习系统中通常有两个层次的 IR 抽象。TensorFlow 的 XLA 和 Intel 的 ngraph 都使用计算图表示,它是高级的表示,有助于执行通用优化,例如内存重用、布局转换和自动微分。 + +TVM 采用底层表示,明确表示内存布局、并行化模式、局部性和硬件原语等选择。低级 IR 更类似 target 硬件——采用了现有图像处理语言,如 Halide、darkroom 和循环转化工具(如 loopy 和基于多面体的分析)的想法。重点关注如何表达深度学习工作负载(如 recurrence)、不同硬件后端的优化,以及如何嵌入框架,从而提供端到端的编译堆栈。 + +## TVM 与 libDNN、cuDNN 的关系 +TVM 将这些库作为外部调用。TVM 的目标之一是生成高性能内核。通过学习手动内核制作技术,并将它们作为原语添加到 DSL 的方式,我们得以增量发展 TVM。有关 TVM 中算子的组成,参见顶部。 + +## 安全 +参阅 [安全指南](/docs/arch/arch/security) diff --git a/versioned_docs/version-0.12.0/how_to/_category_.json b/versioned_docs/version-0.12.0/how_to/_category_.json new file mode 100644 index 00000000..16dd4f29 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 120 +} diff --git a/versioned_docs/version-0.12.0/how_to/autoscheduler/01-autoschedule_gpu.md b/versioned_docs/version-0.12.0/how_to/autoscheduler/01-autoschedule_gpu.md new file mode 100644 index 00000000..9df940e2 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/autoscheduler/01-autoschedule_gpu.md @@ -0,0 +1,1214 @@ +--- +title: 为 GPU 自动调度卷积层 +--- + +# 为 GPU 自动调度卷积层 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/tune_with_autoscheduler/tune_conv2d_layer_cuda.html#sphx-glr-download-how-to-tune-with-autoscheduler-tune-conv2d-layer-cuda-py) 下载完整的示例代码 +::: + +**作者**:[Lianmin Zheng](https://github.com/merrymercy), [Chengfan Jia](https://github.com/jcf94/) + +本文介绍如何为 GPU 使用 auto-scheduler。 + +与 [AutoTVM](/docs/how_to/autotune) 不同,AutoTVM 依赖手动模板来定义搜索空间,而 auto-scheduler 不需要任何模板。用户只需编写计算声明,无需任何调度命令或模板。auto-scheduler 可以自动生成一个大的搜索空间,并在空间中找到合适的调度。 + +``` python +import os +import numpy as np +import tvm +from tvm import te, auto_scheduler, topi +from tvm.topi.testing import conv2d_nchw_python +``` + +## 定义计算 + +首先定义卷积层的计算,该函数返回输入/输出张量列表,从这些张量中,auto-scheduler 可以得到整个计算图。 + +``` python +@auto_scheduler.register_workload +def conv2d_layer(N, H, W, CO, CI, KH, KW, stride, padding): + data = te.placeholder((N, CI, H, W), name="data") + kernel = te.placeholder((CO, CI, KH, KW), name="kernel") + bias = te.placeholder((1, CO, 1, 1), name="bias") + conv = topi.nn.conv2d_nchw(data, kernel, stride, padding, dilation=1, out_dtype="float32") + out = topi.nn.relu(conv + bias) + return [data, kernel, bias, out] +``` + +## 创建搜索任务 + +然后为 ResNet 中的最后一个卷积层创建一个搜索任务。 + +``` python +target = tvm.target.Target("cuda") + +# 使用 ResNet-50 中的最后一层 +N, H, W, CO, CI, KH, KW, strides, padding = 1, 7, 7, 512, 512, 3, 3, (1, 1), (1, 1) +task = auto_scheduler.SearchTask( + func=conv2d_layer, args=(N, H, W, CO, CI, KH, KW, strides, padding), target=target +) + +# 检查计算图 +print("Computational DAG:") +print(task.compute_dag) +``` + +输出结果: + +``` bash +Computational DAG: +data = PLACEHOLDER [1, 512, 7, 7] +pad_temp(i0, i1, i2, i3) = tir.if_then_else(((((i2 >= 1) && (i2 < 8)) && (i3 >= 1)) && (i3 < 8)), data[i0, i1, (i2 - 1), (i3 - 1)], 0f) +kernel = PLACEHOLDER [512, 512, 3, 3] +conv2d_nchw(nn, ff, yy, xx) += (pad_temp[nn, rc, (yy + ry), (xx + rx)]*kernel[ff, rc, ry, rx]) +bias = PLACEHOLDER [1, 512, 1, 1] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nchw[ax0, ax1, ax2, ax3] + bias[ax0, ax1, 0, 0]) +compute(i0, i1, i2, i3) = max(T_add[i0, i1, i2, i3], 0f) +``` + +接下来为 auto-scheduler 设置参数,它们主要指定在搜索过程中如何进行测试。 + +* `measure_ctx` 启动不同进程从而使测试隔离。它保护主进程在测试期间免受 GPU 崩溃影响,并避免其他 runtime 冲突。 +* `min_repeat_ms` 定义每次测试中一次“重复”的最短持续时间。这样可以预热 GPU,从而获得准确测试结果。通常推荐将其值设置为 >= 300 ms。 +* `num_measure_trials` 是搜索过程中可以使用的测试次数。为了快速演示,本教程中只进行了 10 次。推荐实际运行时试验 1000 次,也可以根据时间预算进行更多试验。 +* 此外,使用 `RecordToFile` 将测试记录转储到文件 conv2d.json 中。测试记录可用于查询历史最佳、恢复搜索以及以后进行更多分析。 +* 有关更多参数,参见 `auto_scheduler.TuningOptions`, `auto_scheduler.LocalRPCMeasureContext`。 + +``` python +log_file = "conv2d.json" +measure_ctx = auto_scheduler.LocalRPCMeasureContext(min_repeat_ms=300) +tune_option = auto_scheduler.TuningOptions( + num_measure_trials=10, # change this to 1000 to achieve the best performance + runner=measure_ctx.runner, + measure_callbacks=[auto_scheduler.RecordToFile(log_file)], + verbose=2, +) +``` + +输出结果: + +``` bash +Get devices for measurement successfully! +``` + +## 运行搜索 + +准备好所有输入后,可以开始搜索,让 auto-scheduler 发挥作用。经过一些测试试验后,可以从日志文件中加载最佳 schedule 并进行应用。 + +``` python +# 运行自动调优(搜索) +task.tune(tune_option) +# 应用最佳 schedule +sch, args = task.apply_best(log_file) +# 终止测试过程 +del measure_ctx +``` + +在自动调度后降低调度以查看 IR,auto-scheduler 正确执行优化,包括多级平铺、协作获取、展开和算子融合。 + +``` python +print("Lowered TIR:") +print(tvm.lower(sch, args, simple_mode=True)) +``` + +输出结果: + +``` bash +Lowered TIR: +@main = primfn(data_1: handle, kernel_1: handle, bias_1: handle, compute_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {data: Buffer(data_2: Pointer(float32), float32, [25088], []), + kernel: Buffer(kernel_2: Pointer(float32), float32, [2359296], []), + bias: Buffer(bias_2: Pointer(float32), float32, [512], []), + compute: Buffer(compute_2: Pointer(float32), float32, [25088], [])} + buffer_map = {data_1: data, kernel_1: kernel, bias_1: bias, compute_1: compute} + preflattened_buffer_map = {data_1: data_3: Buffer(data_2, float32, [1, 512, 7, 7], []), kernel_1: kernel_3: Buffer(kernel_2, float32, [512, 512, 3, 3], []), bias_1: bias_3: Buffer(bias_2, float32, [1, 512, 1, 1], []), compute_1: compute_3: Buffer(compute_2, float32, [1, 512, 7, 7], [])} { + attr [IterVar(blockIdx.x: int32, (nullptr), "ThreadIndex", "blockIdx.x")] "thread_extent" = 28; + allocate(conv2d_nchw: Pointer(local float32), float32, [14]), storage_scope = local; + allocate(pad_temp.shared: Pointer(shared float32), float32, [72]), storage_scope = shared; + allocate(kernel.shared: Pointer(shared float32), float32, [3072]), storage_scope = shared; + attr [IterVar(threadIdx.x: int32, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64 { + conv2d_nchw_1: Buffer(conv2d_nchw, float32, [14], [], scope="local", align=32)[0] = 0f32 + conv2d_nchw_1[1] = 0f32 + conv2d_nchw_1[2] = 0f32 + conv2d_nchw_1[3] = 0f32 + conv2d_nchw_1[4] = 0f32 + conv2d_nchw_1[5] = 0f32 + conv2d_nchw_1[6] = 0f32 + conv2d_nchw_1[7] = 0f32 + conv2d_nchw_1[8] = 0f32 + conv2d_nchw_1[9] = 0f32 + conv2d_nchw_1[10] = 0f32 + conv2d_nchw_1[11] = 0f32 + conv2d_nchw_1[12] = 0f32 + conv2d_nchw_1[13] = 0f32 + for (rc.outer.outer: int32, 0, 64) { + for (ry.outer.outer: int32, 0, 3) { + let cse_var_2: int32 = (rc.outer.outer*72) + let cse_var_1: int32 = (ry.outer.outer*3) + { + attr [IterVar(threadIdx.x_1: int32, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64 { + if @tir.likely((threadIdx.x_1 < 18), dtype=bool) { + pad_temp.shared_1: Buffer(pad_temp.shared, float32, [72], [], scope="shared")[(threadIdx.x_1*4)] = @tir.if_then_else(((((1 <= (ry.outer.outer + floormod(blockIdx.x, 7))) && ((ry.outer.outer + floormod(blockIdx.x, 7)) < 8)) && (1 <= floormod((threadIdx.x_1*4), 9))) && (floormod((threadIdx.x_1*4), 9) < 8)), data[((((((rc.outer.outer*392) + (floordiv((threadIdx.x_1*4), 9)*49)) + (ry.outer.outer*7)) + (floormod(blockIdx.x, 7)*7)) + floormod((threadIdx.x_1*4), 9)) - 8)], 0f32, dtype=float32) + } + if @tir.likely((threadIdx.x_1 < 18), dtype=bool) { + pad_temp.shared_1[((threadIdx.x_1*4) + 1)] = @tir.if_then_else(((((1 <= (ry.outer.outer + floormod(blockIdx.x, 7))) && ((ry.outer.outer + floormod(blockIdx.x, 7)) < 8)) && (1 <= floormod(((threadIdx.x_1*4) + 1), 9))) && (floormod(((threadIdx.x_1*4) + 1), 9) < 8)), data[((((((rc.outer.outer*392) + (floordiv(((threadIdx.x_1*4) + 1), 9)*49)) + (ry.outer.outer*7)) + (floormod(blockIdx.x, 7)*7)) + floormod(((threadIdx.x_1*4) + 1), 9)) - 8)], 0f32, dtype=float32) + } + if @tir.likely((threadIdx.x_1 < 18), dtype=bool) { + pad_temp.shared_1[((threadIdx.x_1*4) + 2)] = @tir.if_then_else(((((1 <= (ry.outer.outer + floormod(blockIdx.x, 7))) && ((ry.outer.outer + floormod(blockIdx.x, 7)) < 8)) && (1 <= floormod(((threadIdx.x_1*4) + 2), 9))) && (floormod(((threadIdx.x_1*4) + 2), 9) < 8)), data[((((((rc.outer.outer*392) + (floordiv(((threadIdx.x_1*4) + 2), 9)*49)) + (ry.outer.outer*7)) + (floormod(blockIdx.x, 7)*7)) + floormod(((threadIdx.x_1*4) + 2), 9)) - 8)], 0f32, dtype=float32) + } + if @tir.likely((threadIdx.x_1 < 18), dtype=bool) { + pad_temp.shared_1[((threadIdx.x_1*4) + 3)] = @tir.if_then_else(((((1 <= (ry.outer.outer + floormod(blockIdx.x, 7))) && ((ry.outer.outer + floormod(blockIdx.x, 7)) < 8)) && (1 <= floormod(((threadIdx.x_1*4) + 3), 9))) && (floormod(((threadIdx.x_1*4) + 3), 9) < 8)), data[((((((rc.outer.outer*392) + (floordiv(((threadIdx.x_1*4) + 3), 9)*49)) + (ry.outer.outer*7)) + (floormod(blockIdx.x, 7)*7)) + floormod(((threadIdx.x_1*4) + 3), 9)) - 8)], 0f32, dtype=float32) + } + } + attr [IterVar(threadIdx.x_2: int32, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1: Buffer(kernel.shared, float32, [3072], [], scope="shared")[threadIdx.x_2] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv(threadIdx.x_2, 24)*4608)) + cse_var_2) + (floordiv(floormod(threadIdx.x_2, 24), 3)*9)) + cse_var_1) + floormod(threadIdx.x_2, 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 64)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 64), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 16), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 1), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 128)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 128), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 8), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 2), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 192)] = kernel[(((((((floordiv(blockIdx.x, 7)*589824) + (floordiv(threadIdx.x_2, 24)*4608)) + cse_var_2) + (floordiv(floormod(threadIdx.x_2, 24), 3)*9)) + cse_var_1) + floormod(threadIdx.x_2, 3)) + 36864)] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 256)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 256), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 16), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 1), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 320)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 320), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 8), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 2), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 384)] = kernel[(((((((floordiv(blockIdx.x, 7)*589824) + (floordiv(threadIdx.x_2, 24)*4608)) + cse_var_2) + (floordiv(floormod(threadIdx.x_2, 24), 3)*9)) + cse_var_1) + floormod(threadIdx.x_2, 3)) + 73728)] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 448)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 448), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 16), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 1), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 512)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 512), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 8), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 2), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 576)] = kernel[(((((((floordiv(blockIdx.x, 7)*589824) + (floordiv(threadIdx.x_2, 24)*4608)) + cse_var_2) + (floordiv(floormod(threadIdx.x_2, 24), 3)*9)) + cse_var_1) + floormod(threadIdx.x_2, 3)) + 110592)] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 640)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 640), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 16), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 1), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 704)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 704), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 8), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 2), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 768)] = kernel[(((((((floordiv(blockIdx.x, 7)*589824) + (floordiv(threadIdx.x_2, 24)*4608)) + cse_var_2) + (floordiv(floormod(threadIdx.x_2, 24), 3)*9)) + cse_var_1) + floormod(threadIdx.x_2, 3)) + 147456)] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 832)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 832), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 16), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 1), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 896)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 896), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 8), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 2), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 960)] = kernel[(((((((floordiv(blockIdx.x, 7)*589824) + (floordiv(threadIdx.x_2, 24)*4608)) + cse_var_2) + (floordiv(floormod(threadIdx.x_2, 24), 3)*9)) + cse_var_1) + floormod(threadIdx.x_2, 3)) + 184320)] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 1024)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 1024), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 16), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 1), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 1088)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 1088), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 8), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 2), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 1152)] = kernel[(((((((floordiv(blockIdx.x, 7)*589824) + (floordiv(threadIdx.x_2, 24)*4608)) + cse_var_2) + (floordiv(floormod(threadIdx.x_2, 24), 3)*9)) + cse_var_1) + floormod(threadIdx.x_2, 3)) + 221184)] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 1216)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 1216), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 16), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 1), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 1280)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 1280), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 8), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 2), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 1344)] = kernel[(((((((floordiv(blockIdx.x, 7)*589824) + (floordiv(threadIdx.x_2, 24)*4608)) + cse_var_2) + (floordiv(floormod(threadIdx.x_2, 24), 3)*9)) + cse_var_1) + floormod(threadIdx.x_2, 3)) + 258048)] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 1408)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 1408), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 16), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 1), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 1472)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 1472), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 8), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 2), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 1536)] = kernel[(((((((floordiv(blockIdx.x, 7)*589824) + (floordiv(threadIdx.x_2, 24)*4608)) + cse_var_2) + (floordiv(floormod(threadIdx.x_2, 24), 3)*9)) + cse_var_1) + floormod(threadIdx.x_2, 3)) + 294912)] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 1600)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 1600), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 16), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 1), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 1664)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 1664), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 8), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 2), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 1728)] = kernel[(((((((floordiv(blockIdx.x, 7)*589824) + (floordiv(threadIdx.x_2, 24)*4608)) + cse_var_2) + (floordiv(floormod(threadIdx.x_2, 24), 3)*9)) + cse_var_1) + floormod(threadIdx.x_2, 3)) + 331776)] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 1792)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 1792), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 16), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 1), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 1856)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 1856), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 8), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 2), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 1920)] = kernel[(((((((floordiv(blockIdx.x, 7)*589824) + (floordiv(threadIdx.x_2, 24)*4608)) + cse_var_2) + (floordiv(floormod(threadIdx.x_2, 24), 3)*9)) + cse_var_1) + floormod(threadIdx.x_2, 3)) + 368640)] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 1984)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 1984), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 16), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 1), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 2048)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 2048), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 8), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 2), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 2112)] = kernel[(((((((floordiv(blockIdx.x, 7)*589824) + (floordiv(threadIdx.x_2, 24)*4608)) + cse_var_2) + (floordiv(floormod(threadIdx.x_2, 24), 3)*9)) + cse_var_1) + floormod(threadIdx.x_2, 3)) + 405504)] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 2176)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 2176), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 16), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 1), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 2240)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 2240), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 8), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 2), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 2304)] = kernel[(((((((floordiv(blockIdx.x, 7)*589824) + (floordiv(threadIdx.x_2, 24)*4608)) + cse_var_2) + (floordiv(floormod(threadIdx.x_2, 24), 3)*9)) + cse_var_1) + floormod(threadIdx.x_2, 3)) + 442368)] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 2368)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 2368), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 16), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 1), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 2432)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 2432), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 8), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 2), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 2496)] = kernel[(((((((floordiv(blockIdx.x, 7)*589824) + (floordiv(threadIdx.x_2, 24)*4608)) + cse_var_2) + (floordiv(floormod(threadIdx.x_2, 24), 3)*9)) + cse_var_1) + floormod(threadIdx.x_2, 3)) + 479232)] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 2560)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 2560), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 16), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 1), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 2624)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 2624), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 8), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 2), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 2688)] = kernel[(((((((floordiv(blockIdx.x, 7)*589824) + (floordiv(threadIdx.x_2, 24)*4608)) + cse_var_2) + (floordiv(floormod(threadIdx.x_2, 24), 3)*9)) + cse_var_1) + floormod(threadIdx.x_2, 3)) + 516096)] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 2752)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 2752), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 16), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 1), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 2816)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 2816), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 8), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 2), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 2880)] = kernel[(((((((floordiv(blockIdx.x, 7)*589824) + (floordiv(threadIdx.x_2, 24)*4608)) + cse_var_2) + (floordiv(floormod(threadIdx.x_2, 24), 3)*9)) + cse_var_1) + floormod(threadIdx.x_2, 3)) + 552960)] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 2944)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 2944), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 16), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 1), 3))] + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + kernel.shared_1[(threadIdx.x_2 + 3008)] = kernel[((((((floordiv(blockIdx.x, 7)*589824) + (floordiv((threadIdx.x_2 + 3008), 24)*4608)) + cse_var_2) + (floordiv(floormod((threadIdx.x_2 + 8), 24), 3)*9)) + cse_var_1) + floormod((threadIdx.x_2 + 2), 3))] + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[0]*kernel.shared_1[(threadIdx.x*48)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[9]*kernel.shared_1[((threadIdx.x*48) + 3)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[1]*kernel.shared_1[(threadIdx.x*48)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[10]*kernel.shared_1[((threadIdx.x*48) + 3)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[2]*kernel.shared_1[(threadIdx.x*48)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[11]*kernel.shared_1[((threadIdx.x*48) + 3)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[3]*kernel.shared_1[(threadIdx.x*48)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[12]*kernel.shared_1[((threadIdx.x*48) + 3)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[4]*kernel.shared_1[(threadIdx.x*48)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[13]*kernel.shared_1[((threadIdx.x*48) + 3)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[5]*kernel.shared_1[(threadIdx.x*48)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[14]*kernel.shared_1[((threadIdx.x*48) + 3)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[6]*kernel.shared_1[(threadIdx.x*48)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[15]*kernel.shared_1[((threadIdx.x*48) + 3)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[0]*kernel.shared_1[((threadIdx.x*48) + 24)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[9]*kernel.shared_1[((threadIdx.x*48) + 27)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[1]*kernel.shared_1[((threadIdx.x*48) + 24)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[10]*kernel.shared_1[((threadIdx.x*48) + 27)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[2]*kernel.shared_1[((threadIdx.x*48) + 24)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[11]*kernel.shared_1[((threadIdx.x*48) + 27)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[3]*kernel.shared_1[((threadIdx.x*48) + 24)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[12]*kernel.shared_1[((threadIdx.x*48) + 27)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[4]*kernel.shared_1[((threadIdx.x*48) + 24)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[13]*kernel.shared_1[((threadIdx.x*48) + 27)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[5]*kernel.shared_1[((threadIdx.x*48) + 24)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[14]*kernel.shared_1[((threadIdx.x*48) + 27)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[6]*kernel.shared_1[((threadIdx.x*48) + 24)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[15]*kernel.shared_1[((threadIdx.x*48) + 27)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[1]*kernel.shared_1[((threadIdx.x*48) + 1)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[10]*kernel.shared_1[((threadIdx.x*48) + 4)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[2]*kernel.shared_1[((threadIdx.x*48) + 1)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[11]*kernel.shared_1[((threadIdx.x*48) + 4)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[3]*kernel.shared_1[((threadIdx.x*48) + 1)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[12]*kernel.shared_1[((threadIdx.x*48) + 4)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[4]*kernel.shared_1[((threadIdx.x*48) + 1)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[13]*kernel.shared_1[((threadIdx.x*48) + 4)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[5]*kernel.shared_1[((threadIdx.x*48) + 1)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[14]*kernel.shared_1[((threadIdx.x*48) + 4)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[6]*kernel.shared_1[((threadIdx.x*48) + 1)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[15]*kernel.shared_1[((threadIdx.x*48) + 4)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[7]*kernel.shared_1[((threadIdx.x*48) + 1)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[16]*kernel.shared_1[((threadIdx.x*48) + 4)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[1]*kernel.shared_1[((threadIdx.x*48) + 25)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[10]*kernel.shared_1[((threadIdx.x*48) + 28)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[2]*kernel.shared_1[((threadIdx.x*48) + 25)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[11]*kernel.shared_1[((threadIdx.x*48) + 28)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[3]*kernel.shared_1[((threadIdx.x*48) + 25)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[12]*kernel.shared_1[((threadIdx.x*48) + 28)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[4]*kernel.shared_1[((threadIdx.x*48) + 25)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[13]*kernel.shared_1[((threadIdx.x*48) + 28)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[5]*kernel.shared_1[((threadIdx.x*48) + 25)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[14]*kernel.shared_1[((threadIdx.x*48) + 28)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[6]*kernel.shared_1[((threadIdx.x*48) + 25)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[15]*kernel.shared_1[((threadIdx.x*48) + 28)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[7]*kernel.shared_1[((threadIdx.x*48) + 25)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[16]*kernel.shared_1[((threadIdx.x*48) + 28)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[2]*kernel.shared_1[((threadIdx.x*48) + 2)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[11]*kernel.shared_1[((threadIdx.x*48) + 5)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[3]*kernel.shared_1[((threadIdx.x*48) + 2)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[12]*kernel.shared_1[((threadIdx.x*48) + 5)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[4]*kernel.shared_1[((threadIdx.x*48) + 2)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[13]*kernel.shared_1[((threadIdx.x*48) + 5)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[5]*kernel.shared_1[((threadIdx.x*48) + 2)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[14]*kernel.shared_1[((threadIdx.x*48) + 5)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[6]*kernel.shared_1[((threadIdx.x*48) + 2)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[15]*kernel.shared_1[((threadIdx.x*48) + 5)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[7]*kernel.shared_1[((threadIdx.x*48) + 2)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[16]*kernel.shared_1[((threadIdx.x*48) + 5)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[8]*kernel.shared_1[((threadIdx.x*48) + 2)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[17]*kernel.shared_1[((threadIdx.x*48) + 5)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[2]*kernel.shared_1[((threadIdx.x*48) + 26)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[11]*kernel.shared_1[((threadIdx.x*48) + 29)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[3]*kernel.shared_1[((threadIdx.x*48) + 26)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[12]*kernel.shared_1[((threadIdx.x*48) + 29)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[4]*kernel.shared_1[((threadIdx.x*48) + 26)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[13]*kernel.shared_1[((threadIdx.x*48) + 29)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[5]*kernel.shared_1[((threadIdx.x*48) + 26)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[14]*kernel.shared_1[((threadIdx.x*48) + 29)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[6]*kernel.shared_1[((threadIdx.x*48) + 26)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[15]*kernel.shared_1[((threadIdx.x*48) + 29)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[7]*kernel.shared_1[((threadIdx.x*48) + 26)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[16]*kernel.shared_1[((threadIdx.x*48) + 29)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[8]*kernel.shared_1[((threadIdx.x*48) + 26)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[17]*kernel.shared_1[((threadIdx.x*48) + 29)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[18]*kernel.shared_1[((threadIdx.x*48) + 6)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[27]*kernel.shared_1[((threadIdx.x*48) + 9)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[19]*kernel.shared_1[((threadIdx.x*48) + 6)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[28]*kernel.shared_1[((threadIdx.x*48) + 9)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[20]*kernel.shared_1[((threadIdx.x*48) + 6)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[29]*kernel.shared_1[((threadIdx.x*48) + 9)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[21]*kernel.shared_1[((threadIdx.x*48) + 6)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[30]*kernel.shared_1[((threadIdx.x*48) + 9)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[22]*kernel.shared_1[((threadIdx.x*48) + 6)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[31]*kernel.shared_1[((threadIdx.x*48) + 9)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[23]*kernel.shared_1[((threadIdx.x*48) + 6)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[32]*kernel.shared_1[((threadIdx.x*48) + 9)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[24]*kernel.shared_1[((threadIdx.x*48) + 6)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[33]*kernel.shared_1[((threadIdx.x*48) + 9)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[18]*kernel.shared_1[((threadIdx.x*48) + 30)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[27]*kernel.shared_1[((threadIdx.x*48) + 33)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[19]*kernel.shared_1[((threadIdx.x*48) + 30)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[28]*kernel.shared_1[((threadIdx.x*48) + 33)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[20]*kernel.shared_1[((threadIdx.x*48) + 30)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[29]*kernel.shared_1[((threadIdx.x*48) + 33)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[21]*kernel.shared_1[((threadIdx.x*48) + 30)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[30]*kernel.shared_1[((threadIdx.x*48) + 33)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[22]*kernel.shared_1[((threadIdx.x*48) + 30)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[31]*kernel.shared_1[((threadIdx.x*48) + 33)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[23]*kernel.shared_1[((threadIdx.x*48) + 30)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[32]*kernel.shared_1[((threadIdx.x*48) + 33)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[24]*kernel.shared_1[((threadIdx.x*48) + 30)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[33]*kernel.shared_1[((threadIdx.x*48) + 33)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[19]*kernel.shared_1[((threadIdx.x*48) + 7)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[28]*kernel.shared_1[((threadIdx.x*48) + 10)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[20]*kernel.shared_1[((threadIdx.x*48) + 7)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[29]*kernel.shared_1[((threadIdx.x*48) + 10)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[21]*kernel.shared_1[((threadIdx.x*48) + 7)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[30]*kernel.shared_1[((threadIdx.x*48) + 10)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[22]*kernel.shared_1[((threadIdx.x*48) + 7)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[31]*kernel.shared_1[((threadIdx.x*48) + 10)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[23]*kernel.shared_1[((threadIdx.x*48) + 7)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[32]*kernel.shared_1[((threadIdx.x*48) + 10)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[24]*kernel.shared_1[((threadIdx.x*48) + 7)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[33]*kernel.shared_1[((threadIdx.x*48) + 10)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[25]*kernel.shared_1[((threadIdx.x*48) + 7)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[34]*kernel.shared_1[((threadIdx.x*48) + 10)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[19]*kernel.shared_1[((threadIdx.x*48) + 31)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[28]*kernel.shared_1[((threadIdx.x*48) + 34)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[20]*kernel.shared_1[((threadIdx.x*48) + 31)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[29]*kernel.shared_1[((threadIdx.x*48) + 34)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[21]*kernel.shared_1[((threadIdx.x*48) + 31)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[30]*kernel.shared_1[((threadIdx.x*48) + 34)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[22]*kernel.shared_1[((threadIdx.x*48) + 31)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[31]*kernel.shared_1[((threadIdx.x*48) + 34)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[23]*kernel.shared_1[((threadIdx.x*48) + 31)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[32]*kernel.shared_1[((threadIdx.x*48) + 34)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[24]*kernel.shared_1[((threadIdx.x*48) + 31)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[33]*kernel.shared_1[((threadIdx.x*48) + 34)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[25]*kernel.shared_1[((threadIdx.x*48) + 31)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[34]*kernel.shared_1[((threadIdx.x*48) + 34)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[20]*kernel.shared_1[((threadIdx.x*48) + 8)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[29]*kernel.shared_1[((threadIdx.x*48) + 11)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[21]*kernel.shared_1[((threadIdx.x*48) + 8)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[30]*kernel.shared_1[((threadIdx.x*48) + 11)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[22]*kernel.shared_1[((threadIdx.x*48) + 8)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[31]*kernel.shared_1[((threadIdx.x*48) + 11)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[23]*kernel.shared_1[((threadIdx.x*48) + 8)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[32]*kernel.shared_1[((threadIdx.x*48) + 11)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[24]*kernel.shared_1[((threadIdx.x*48) + 8)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[33]*kernel.shared_1[((threadIdx.x*48) + 11)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[25]*kernel.shared_1[((threadIdx.x*48) + 8)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[34]*kernel.shared_1[((threadIdx.x*48) + 11)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[26]*kernel.shared_1[((threadIdx.x*48) + 8)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[35]*kernel.shared_1[((threadIdx.x*48) + 11)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[20]*kernel.shared_1[((threadIdx.x*48) + 32)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[29]*kernel.shared_1[((threadIdx.x*48) + 35)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[21]*kernel.shared_1[((threadIdx.x*48) + 32)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[30]*kernel.shared_1[((threadIdx.x*48) + 35)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[22]*kernel.shared_1[((threadIdx.x*48) + 32)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[31]*kernel.shared_1[((threadIdx.x*48) + 35)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[23]*kernel.shared_1[((threadIdx.x*48) + 32)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[32]*kernel.shared_1[((threadIdx.x*48) + 35)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[24]*kernel.shared_1[((threadIdx.x*48) + 32)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[33]*kernel.shared_1[((threadIdx.x*48) + 35)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[25]*kernel.shared_1[((threadIdx.x*48) + 32)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[34]*kernel.shared_1[((threadIdx.x*48) + 35)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[26]*kernel.shared_1[((threadIdx.x*48) + 32)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[35]*kernel.shared_1[((threadIdx.x*48) + 35)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[36]*kernel.shared_1[((threadIdx.x*48) + 12)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[45]*kernel.shared_1[((threadIdx.x*48) + 15)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[37]*kernel.shared_1[((threadIdx.x*48) + 12)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[46]*kernel.shared_1[((threadIdx.x*48) + 15)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[38]*kernel.shared_1[((threadIdx.x*48) + 12)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[47]*kernel.shared_1[((threadIdx.x*48) + 15)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[39]*kernel.shared_1[((threadIdx.x*48) + 12)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[48]*kernel.shared_1[((threadIdx.x*48) + 15)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[40]*kernel.shared_1[((threadIdx.x*48) + 12)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[49]*kernel.shared_1[((threadIdx.x*48) + 15)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[41]*kernel.shared_1[((threadIdx.x*48) + 12)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[50]*kernel.shared_1[((threadIdx.x*48) + 15)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[42]*kernel.shared_1[((threadIdx.x*48) + 12)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[51]*kernel.shared_1[((threadIdx.x*48) + 15)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[36]*kernel.shared_1[((threadIdx.x*48) + 36)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[45]*kernel.shared_1[((threadIdx.x*48) + 39)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[37]*kernel.shared_1[((threadIdx.x*48) + 36)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[46]*kernel.shared_1[((threadIdx.x*48) + 39)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[38]*kernel.shared_1[((threadIdx.x*48) + 36)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[47]*kernel.shared_1[((threadIdx.x*48) + 39)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[39]*kernel.shared_1[((threadIdx.x*48) + 36)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[48]*kernel.shared_1[((threadIdx.x*48) + 39)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[40]*kernel.shared_1[((threadIdx.x*48) + 36)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[49]*kernel.shared_1[((threadIdx.x*48) + 39)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[41]*kernel.shared_1[((threadIdx.x*48) + 36)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[50]*kernel.shared_1[((threadIdx.x*48) + 39)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[42]*kernel.shared_1[((threadIdx.x*48) + 36)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[51]*kernel.shared_1[((threadIdx.x*48) + 39)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[37]*kernel.shared_1[((threadIdx.x*48) + 13)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[46]*kernel.shared_1[((threadIdx.x*48) + 16)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[38]*kernel.shared_1[((threadIdx.x*48) + 13)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[47]*kernel.shared_1[((threadIdx.x*48) + 16)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[39]*kernel.shared_1[((threadIdx.x*48) + 13)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[48]*kernel.shared_1[((threadIdx.x*48) + 16)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[40]*kernel.shared_1[((threadIdx.x*48) + 13)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[49]*kernel.shared_1[((threadIdx.x*48) + 16)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[41]*kernel.shared_1[((threadIdx.x*48) + 13)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[50]*kernel.shared_1[((threadIdx.x*48) + 16)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[42]*kernel.shared_1[((threadIdx.x*48) + 13)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[51]*kernel.shared_1[((threadIdx.x*48) + 16)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[43]*kernel.shared_1[((threadIdx.x*48) + 13)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[52]*kernel.shared_1[((threadIdx.x*48) + 16)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[37]*kernel.shared_1[((threadIdx.x*48) + 37)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[46]*kernel.shared_1[((threadIdx.x*48) + 40)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[38]*kernel.shared_1[((threadIdx.x*48) + 37)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[47]*kernel.shared_1[((threadIdx.x*48) + 40)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[39]*kernel.shared_1[((threadIdx.x*48) + 37)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[48]*kernel.shared_1[((threadIdx.x*48) + 40)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[40]*kernel.shared_1[((threadIdx.x*48) + 37)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[49]*kernel.shared_1[((threadIdx.x*48) + 40)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[41]*kernel.shared_1[((threadIdx.x*48) + 37)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[50]*kernel.shared_1[((threadIdx.x*48) + 40)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[42]*kernel.shared_1[((threadIdx.x*48) + 37)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[51]*kernel.shared_1[((threadIdx.x*48) + 40)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[43]*kernel.shared_1[((threadIdx.x*48) + 37)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[52]*kernel.shared_1[((threadIdx.x*48) + 40)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[38]*kernel.shared_1[((threadIdx.x*48) + 14)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[47]*kernel.shared_1[((threadIdx.x*48) + 17)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[39]*kernel.shared_1[((threadIdx.x*48) + 14)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[48]*kernel.shared_1[((threadIdx.x*48) + 17)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[40]*kernel.shared_1[((threadIdx.x*48) + 14)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[49]*kernel.shared_1[((threadIdx.x*48) + 17)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[41]*kernel.shared_1[((threadIdx.x*48) + 14)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[50]*kernel.shared_1[((threadIdx.x*48) + 17)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[42]*kernel.shared_1[((threadIdx.x*48) + 14)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[51]*kernel.shared_1[((threadIdx.x*48) + 17)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[43]*kernel.shared_1[((threadIdx.x*48) + 14)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[52]*kernel.shared_1[((threadIdx.x*48) + 17)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[44]*kernel.shared_1[((threadIdx.x*48) + 14)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[53]*kernel.shared_1[((threadIdx.x*48) + 17)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[38]*kernel.shared_1[((threadIdx.x*48) + 38)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[47]*kernel.shared_1[((threadIdx.x*48) + 41)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[39]*kernel.shared_1[((threadIdx.x*48) + 38)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[48]*kernel.shared_1[((threadIdx.x*48) + 41)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[40]*kernel.shared_1[((threadIdx.x*48) + 38)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[49]*kernel.shared_1[((threadIdx.x*48) + 41)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[41]*kernel.shared_1[((threadIdx.x*48) + 38)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[50]*kernel.shared_1[((threadIdx.x*48) + 41)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[42]*kernel.shared_1[((threadIdx.x*48) + 38)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[51]*kernel.shared_1[((threadIdx.x*48) + 41)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[43]*kernel.shared_1[((threadIdx.x*48) + 38)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[52]*kernel.shared_1[((threadIdx.x*48) + 41)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[44]*kernel.shared_1[((threadIdx.x*48) + 38)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[53]*kernel.shared_1[((threadIdx.x*48) + 41)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[54]*kernel.shared_1[((threadIdx.x*48) + 18)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[63]*kernel.shared_1[((threadIdx.x*48) + 21)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[55]*kernel.shared_1[((threadIdx.x*48) + 18)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[64]*kernel.shared_1[((threadIdx.x*48) + 21)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[56]*kernel.shared_1[((threadIdx.x*48) + 18)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[65]*kernel.shared_1[((threadIdx.x*48) + 21)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[57]*kernel.shared_1[((threadIdx.x*48) + 18)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[66]*kernel.shared_1[((threadIdx.x*48) + 21)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[58]*kernel.shared_1[((threadIdx.x*48) + 18)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[67]*kernel.shared_1[((threadIdx.x*48) + 21)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[59]*kernel.shared_1[((threadIdx.x*48) + 18)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[68]*kernel.shared_1[((threadIdx.x*48) + 21)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[60]*kernel.shared_1[((threadIdx.x*48) + 18)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[69]*kernel.shared_1[((threadIdx.x*48) + 21)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[54]*kernel.shared_1[((threadIdx.x*48) + 42)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[63]*kernel.shared_1[((threadIdx.x*48) + 45)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[55]*kernel.shared_1[((threadIdx.x*48) + 42)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[64]*kernel.shared_1[((threadIdx.x*48) + 45)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[56]*kernel.shared_1[((threadIdx.x*48) + 42)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[65]*kernel.shared_1[((threadIdx.x*48) + 45)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[57]*kernel.shared_1[((threadIdx.x*48) + 42)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[66]*kernel.shared_1[((threadIdx.x*48) + 45)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[58]*kernel.shared_1[((threadIdx.x*48) + 42)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[67]*kernel.shared_1[((threadIdx.x*48) + 45)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[59]*kernel.shared_1[((threadIdx.x*48) + 42)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[68]*kernel.shared_1[((threadIdx.x*48) + 45)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[60]*kernel.shared_1[((threadIdx.x*48) + 42)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[69]*kernel.shared_1[((threadIdx.x*48) + 45)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[55]*kernel.shared_1[((threadIdx.x*48) + 19)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[64]*kernel.shared_1[((threadIdx.x*48) + 22)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[56]*kernel.shared_1[((threadIdx.x*48) + 19)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[65]*kernel.shared_1[((threadIdx.x*48) + 22)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[57]*kernel.shared_1[((threadIdx.x*48) + 19)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[66]*kernel.shared_1[((threadIdx.x*48) + 22)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[58]*kernel.shared_1[((threadIdx.x*48) + 19)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[67]*kernel.shared_1[((threadIdx.x*48) + 22)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[59]*kernel.shared_1[((threadIdx.x*48) + 19)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[68]*kernel.shared_1[((threadIdx.x*48) + 22)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[60]*kernel.shared_1[((threadIdx.x*48) + 19)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[69]*kernel.shared_1[((threadIdx.x*48) + 22)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[61]*kernel.shared_1[((threadIdx.x*48) + 19)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[70]*kernel.shared_1[((threadIdx.x*48) + 22)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[55]*kernel.shared_1[((threadIdx.x*48) + 43)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[64]*kernel.shared_1[((threadIdx.x*48) + 46)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[56]*kernel.shared_1[((threadIdx.x*48) + 43)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[65]*kernel.shared_1[((threadIdx.x*48) + 46)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[57]*kernel.shared_1[((threadIdx.x*48) + 43)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[66]*kernel.shared_1[((threadIdx.x*48) + 46)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[58]*kernel.shared_1[((threadIdx.x*48) + 43)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[67]*kernel.shared_1[((threadIdx.x*48) + 46)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[59]*kernel.shared_1[((threadIdx.x*48) + 43)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[68]*kernel.shared_1[((threadIdx.x*48) + 46)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[60]*kernel.shared_1[((threadIdx.x*48) + 43)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[69]*kernel.shared_1[((threadIdx.x*48) + 46)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[61]*kernel.shared_1[((threadIdx.x*48) + 43)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[70]*kernel.shared_1[((threadIdx.x*48) + 46)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[56]*kernel.shared_1[((threadIdx.x*48) + 20)])) + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[65]*kernel.shared_1[((threadIdx.x*48) + 23)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[57]*kernel.shared_1[((threadIdx.x*48) + 20)])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[66]*kernel.shared_1[((threadIdx.x*48) + 23)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[58]*kernel.shared_1[((threadIdx.x*48) + 20)])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[67]*kernel.shared_1[((threadIdx.x*48) + 23)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[59]*kernel.shared_1[((threadIdx.x*48) + 20)])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[68]*kernel.shared_1[((threadIdx.x*48) + 23)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[60]*kernel.shared_1[((threadIdx.x*48) + 20)])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[69]*kernel.shared_1[((threadIdx.x*48) + 23)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[61]*kernel.shared_1[((threadIdx.x*48) + 20)])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[70]*kernel.shared_1[((threadIdx.x*48) + 23)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[62]*kernel.shared_1[((threadIdx.x*48) + 20)])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[71]*kernel.shared_1[((threadIdx.x*48) + 23)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[56]*kernel.shared_1[((threadIdx.x*48) + 44)])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[65]*kernel.shared_1[((threadIdx.x*48) + 47)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[57]*kernel.shared_1[((threadIdx.x*48) + 44)])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[66]*kernel.shared_1[((threadIdx.x*48) + 47)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[58]*kernel.shared_1[((threadIdx.x*48) + 44)])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[67]*kernel.shared_1[((threadIdx.x*48) + 47)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[59]*kernel.shared_1[((threadIdx.x*48) + 44)])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[68]*kernel.shared_1[((threadIdx.x*48) + 47)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[60]*kernel.shared_1[((threadIdx.x*48) + 44)])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[69]*kernel.shared_1[((threadIdx.x*48) + 47)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[61]*kernel.shared_1[((threadIdx.x*48) + 44)])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[70]*kernel.shared_1[((threadIdx.x*48) + 47)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[62]*kernel.shared_1[((threadIdx.x*48) + 44)])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[71]*kernel.shared_1[((threadIdx.x*48) + 47)])) + } + } + } + for (i1.inner: int32, 0, 2) { + for (i3.inner: int32, 0, 7) { + compute[(((((floordiv(blockIdx.x, 7)*6272) + (threadIdx.x*98)) + (i1.inner*49)) + (floormod(blockIdx.x, 7)*7)) + i3.inner)] = max((conv2d_nchw_1[((i1.inner*7) + i3.inner)] + bias[(((floordiv(blockIdx.x, 7)*128) + (threadIdx.x*2)) + i1.inner)]), 0f32) + } + } + } +} +``` + +## 检查正确性并评估性能 + +构建二进制文件,并验证其正确性和性能。 + +``` python +func = tvm.build(sch, args, target) + +# 检查正确性 +data_np = np.random.uniform(size=(N, CI, H, W)).astype(np.float32) +weight_np = np.random.uniform(size=(CO, CI, KH, KW)).astype(np.float32) +bias_np = np.random.uniform(size=(1, CO, 1, 1)).astype(np.float32) +conv_np = conv2d_nchw_python(data_np, weight_np, strides, padding) +out_np = np.maximum(conv_np + bias_np, 0.0) + +dev = tvm.cuda() +data_tvm = tvm.nd.array(data_np, device=dev) +weight_tvm = tvm.nd.array(weight_np, device=dev) +bias_tvm = tvm.nd.array(bias_np, device=dev) +out_tvm = tvm.nd.empty(out_np.shape, device=dev) +func(data_tvm, weight_tvm, bias_tvm, out_tvm) + +# 检查结果 +np.testing.assert_allclose(out_np, out_tvm.numpy(), rtol=1e-3) + +# 评估执行时间 +evaluator = func.time_evaluator(func.entry_name, dev, min_repeat_ms=500) +print( + "Execution time of this operator: %.3f ms" + % (np.median(evaluator(data_tvm, weight_tvm, bias_tvm, out_tvm).results) * 1000) +) +``` + +输出结果: + +``` bash +Execution time of this operator: 0.361 ms +``` + +## 使用记录文件 + +在搜索过程中,所有测试记录(可用于重新应用搜索结果、恢复搜索和执行其他分析)都转储到记录文件「conv2d.json」中。 + +下面示例中从文件中加载最佳调度,打印等效的 Python 调度 API 和 CUDA 源代码(可用于调试和分析自动调度程序的行为)。 + +``` python +print("Equivalent python schedule:") +print(task.print_best(log_file, print_mode="schedule")) + +print("CUDA source code:") +print(task.print_best(log_file, print_mode="cuda")) +``` + +输出结果: + +``` bash +Equivalent python schedule: +pad_temp_i0, pad_temp_i1, pad_temp_i2, pad_temp_i3 = tuple(pad_temp.op.axis) + tuple(pad_temp.op.reduce_axis) +conv2d_nchw_nn, conv2d_nchw_ff, conv2d_nchw_yy, conv2d_nchw_xx, conv2d_nchw_rc, conv2d_nchw_ry, conv2d_nchw_rx = tuple(conv2d_nchw.op.axis) + tuple(conv2d_nchw.op.reduce_axis) +T_add_ax0, T_add_ax1, T_add_ax2, T_add_ax3 = tuple(T_add.op.axis) + tuple(T_add.op.reduce_axis) +compute_i0, compute_i1, compute_i2, compute_i3 = tuple(compute.op.axis) + tuple(compute.op.reduce_axis) +s[T_add].compute_inline() +conv2d_nchw_nn_o_i, conv2d_nchw_nn_i = s[conv2d_nchw].split(conv2d_nchw_nn, factor=1) +conv2d_nchw_nn_o_o_i, conv2d_nchw_nn_o_i = s[conv2d_nchw].split(conv2d_nchw_nn_o_i, factor=1) +conv2d_nchw_nn_o_o_o_i, conv2d_nchw_nn_o_o_i = s[conv2d_nchw].split(conv2d_nchw_nn_o_o_i, factor=1) +conv2d_nchw_nn_o_o_o_o, conv2d_nchw_nn_o_o_o_i = s[conv2d_nchw].split(conv2d_nchw_nn_o_o_o_i, factor=1) +conv2d_nchw_ff_o_i, conv2d_nchw_ff_i = s[conv2d_nchw].split(conv2d_nchw_ff, factor=1) +conv2d_nchw_ff_o_o_i, conv2d_nchw_ff_o_i = s[conv2d_nchw].split(conv2d_nchw_ff_o_i, factor=2) +conv2d_nchw_ff_o_o_o_i, conv2d_nchw_ff_o_o_i = s[conv2d_nchw].split(conv2d_nchw_ff_o_o_i, factor=64) +conv2d_nchw_ff_o_o_o_o, conv2d_nchw_ff_o_o_o_i = s[conv2d_nchw].split(conv2d_nchw_ff_o_o_o_i, factor=1) +conv2d_nchw_yy_o_i, conv2d_nchw_yy_i = s[conv2d_nchw].split(conv2d_nchw_yy, factor=1) +conv2d_nchw_yy_o_o_i, conv2d_nchw_yy_o_i = s[conv2d_nchw].split(conv2d_nchw_yy_o_i, factor=1) +conv2d_nchw_yy_o_o_o_i, conv2d_nchw_yy_o_o_i = s[conv2d_nchw].split(conv2d_nchw_yy_o_o_i, factor=1) +conv2d_nchw_yy_o_o_o_o, conv2d_nchw_yy_o_o_o_i = s[conv2d_nchw].split(conv2d_nchw_yy_o_o_o_i, factor=1) +conv2d_nchw_xx_o_i, conv2d_nchw_xx_i = s[conv2d_nchw].split(conv2d_nchw_xx, factor=1) +conv2d_nchw_xx_o_o_i, conv2d_nchw_xx_o_i = s[conv2d_nchw].split(conv2d_nchw_xx_o_i, factor=7) +conv2d_nchw_xx_o_o_o_i, conv2d_nchw_xx_o_o_i = s[conv2d_nchw].split(conv2d_nchw_xx_o_o_i, factor=1) +conv2d_nchw_xx_o_o_o_o, conv2d_nchw_xx_o_o_o_i = s[conv2d_nchw].split(conv2d_nchw_xx_o_o_o_i, factor=1) +conv2d_nchw_rc_o_i, conv2d_nchw_rc_i = s[conv2d_nchw].split(conv2d_nchw_rc, factor=2) +conv2d_nchw_rc_o_o, conv2d_nchw_rc_o_i = s[conv2d_nchw].split(conv2d_nchw_rc_o_i, factor=4) +conv2d_nchw_ry_o_i, conv2d_nchw_ry_i = s[conv2d_nchw].split(conv2d_nchw_ry, factor=1) +conv2d_nchw_ry_o_o, conv2d_nchw_ry_o_i = s[conv2d_nchw].split(conv2d_nchw_ry_o_i, factor=1) +conv2d_nchw_rx_o_i, conv2d_nchw_rx_i = s[conv2d_nchw].split(conv2d_nchw_rx, factor=1) +conv2d_nchw_rx_o_o, conv2d_nchw_rx_o_i = s[conv2d_nchw].split(conv2d_nchw_rx_o_i, factor=3) +s[conv2d_nchw].reorder(conv2d_nchw_nn_o_o_o_o, conv2d_nchw_ff_o_o_o_o, conv2d_nchw_yy_o_o_o_o, conv2d_nchw_xx_o_o_o_o, conv2d_nchw_nn_o_o_o_i, conv2d_nchw_ff_o_o_o_i, conv2d_nchw_yy_o_o_o_i, conv2d_nchw_xx_o_o_o_i, conv2d_nchw_nn_o_o_i, conv2d_nchw_ff_o_o_i, conv2d_nchw_yy_o_o_i, conv2d_nchw_xx_o_o_i, conv2d_nchw_rc_o_o, conv2d_nchw_ry_o_o, conv2d_nchw_rx_o_o, conv2d_nchw_rc_o_i, conv2d_nchw_ry_o_i, conv2d_nchw_rx_o_i, conv2d_nchw_nn_o_i, conv2d_nchw_ff_o_i, conv2d_nchw_yy_o_i, conv2d_nchw_xx_o_i, conv2d_nchw_rc_i, conv2d_nchw_ry_i, conv2d_nchw_rx_i, conv2d_nchw_nn_i, conv2d_nchw_ff_i, conv2d_nchw_yy_i, conv2d_nchw_xx_i) +compute_i0_o_i, compute_i0_i = s[compute].split(compute_i0, factor=1) +compute_i0_o_o_i, compute_i0_o_i = s[compute].split(compute_i0_o_i, factor=1) +compute_i0_o_o_o, compute_i0_o_o_i = s[compute].split(compute_i0_o_o_i, factor=1) +compute_i1_o_i, compute_i1_i = s[compute].split(compute_i1, factor=2) +compute_i1_o_o_i, compute_i1_o_i = s[compute].split(compute_i1_o_i, factor=64) +compute_i1_o_o_o, compute_i1_o_o_i = s[compute].split(compute_i1_o_o_i, factor=1) +compute_i2_o_i, compute_i2_i = s[compute].split(compute_i2, factor=1) +compute_i2_o_o_i, compute_i2_o_i = s[compute].split(compute_i2_o_i, factor=1) +compute_i2_o_o_o, compute_i2_o_o_i = s[compute].split(compute_i2_o_o_i, factor=1) +compute_i3_o_i, compute_i3_i = s[compute].split(compute_i3, factor=7) +compute_i3_o_o_i, compute_i3_o_i = s[compute].split(compute_i3_o_i, factor=1) +compute_i3_o_o_o, compute_i3_o_o_i = s[compute].split(compute_i3_o_o_i, factor=1) +s[compute].reorder(compute_i0_o_o_o, compute_i1_o_o_o, compute_i2_o_o_o, compute_i3_o_o_o, compute_i0_o_o_i, compute_i1_o_o_i, compute_i2_o_o_i, compute_i3_o_o_i, compute_i0_o_i, compute_i1_o_i, compute_i2_o_i, compute_i3_o_i, compute_i0_i, compute_i1_i, compute_i2_i, compute_i3_i) +s[conv2d_nchw].compute_at(s[compute], compute_i3_o_i) +kernel_shared = s.cache_read(kernel, "shared", [conv2d_nchw]) +kernel_shared_ax0, kernel_shared_ax1, kernel_shared_ax2, kernel_shared_ax3 = tuple(kernel_shared.op.axis) +s[kernel_shared].compute_at(s[conv2d_nchw], conv2d_nchw_rx_o_o) +pad_temp_shared = s.cache_read(pad_temp, "shared", [conv2d_nchw]) +pad_temp_shared_ax0, pad_temp_shared_ax1, pad_temp_shared_ax2, pad_temp_shared_ax3 = tuple(pad_temp_shared.op.axis) +s[pad_temp_shared].compute_at(s[conv2d_nchw], conv2d_nchw_rx_o_o) +s[pad_temp].compute_inline() +compute_i0_o_o_o_i1_o_o_o_fused_i2_o_o_o_fused_i3_o_o_o_fused = s[compute].fuse(compute_i0_o_o_o, compute_i1_o_o_o, compute_i2_o_o_o, compute_i3_o_o_o) +s[compute].bind(compute_i0_o_o_o_i1_o_o_o_fused_i2_o_o_o_fused_i3_o_o_o_fused, te.thread_axis("blockIdx.x")) +compute_i0_o_o_i_i1_o_o_i_fused_i2_o_o_i_fused_i3_o_o_i_fused = s[compute].fuse(compute_i0_o_o_i, compute_i1_o_o_i, compute_i2_o_o_i, compute_i3_o_o_i) +s[compute].bind(compute_i0_o_o_i_i1_o_o_i_fused_i2_o_o_i_fused_i3_o_o_i_fused, te.thread_axis("vthread")) +compute_i0_o_i_i1_o_i_fused_i2_o_i_fused_i3_o_i_fused = s[compute].fuse(compute_i0_o_i, compute_i1_o_i, compute_i2_o_i, compute_i3_o_i) +s[compute].bind(compute_i0_o_i_i1_o_i_fused_i2_o_i_fused_i3_o_i_fused, te.thread_axis("threadIdx.x")) +kernel_shared_ax0_ax1_fused_ax2_fused_ax3_fused = s[kernel_shared].fuse(kernel_shared_ax0, kernel_shared_ax1, kernel_shared_ax2, kernel_shared_ax3) +kernel_shared_ax0_ax1_fused_ax2_fused_ax3_fused_o, kernel_shared_ax0_ax1_fused_ax2_fused_ax3_fused_i = s[kernel_shared].split(kernel_shared_ax0_ax1_fused_ax2_fused_ax3_fused, factor=1) +s[kernel_shared].vectorize(kernel_shared_ax0_ax1_fused_ax2_fused_ax3_fused_i) +kernel_shared_ax0_ax1_fused_ax2_fused_ax3_fused_o_o, kernel_shared_ax0_ax1_fused_ax2_fused_ax3_fused_o_i = s[kernel_shared].split(kernel_shared_ax0_ax1_fused_ax2_fused_ax3_fused_o, factor=64) +s[kernel_shared].bind(kernel_shared_ax0_ax1_fused_ax2_fused_ax3_fused_o_i, te.thread_axis("threadIdx.x")) +pad_temp_shared_ax0_ax1_fused_ax2_fused_ax3_fused = s[pad_temp_shared].fuse(pad_temp_shared_ax0, pad_temp_shared_ax1, pad_temp_shared_ax2, pad_temp_shared_ax3) +pad_temp_shared_ax0_ax1_fused_ax2_fused_ax3_fused_o, pad_temp_shared_ax0_ax1_fused_ax2_fused_ax3_fused_i = s[pad_temp_shared].split(pad_temp_shared_ax0_ax1_fused_ax2_fused_ax3_fused, factor=4) +s[pad_temp_shared].vectorize(pad_temp_shared_ax0_ax1_fused_ax2_fused_ax3_fused_i) +pad_temp_shared_ax0_ax1_fused_ax2_fused_ax3_fused_o_o, pad_temp_shared_ax0_ax1_fused_ax2_fused_ax3_fused_o_i = s[pad_temp_shared].split(pad_temp_shared_ax0_ax1_fused_ax2_fused_ax3_fused_o, factor=64) +s[pad_temp_shared].bind(pad_temp_shared_ax0_ax1_fused_ax2_fused_ax3_fused_o_i, te.thread_axis("threadIdx.x")) +s[conv2d_nchw].pragma(conv2d_nchw_nn_o_o_o_o, "auto_unroll_max_step", 512) +s[conv2d_nchw].pragma(conv2d_nchw_nn_o_o_o_o, "unroll_explicit", True) + +CUDA source code: + +#ifdef _WIN32 + using uint = unsigned int; + using uchar = unsigned char; + using ushort = unsigned short; + using int64_t = long long; + using uint64_t = unsigned long long; +#else + #define uint unsigned int + #define uchar unsigned char + #define ushort unsigned short + #define int64_t long long + #define uint64_t unsigned long long +#endif +extern "C" __global__ void __launch_bounds__(64) default_function_kernel0(float* __restrict__ data, float* __restrict__ kernel, float* __restrict__ compute, float* __restrict__ bias) { + float conv2d_nchw[14]; + __shared__ float pad_temp_shared[72]; + __shared__ float kernel_shared[3072]; + conv2d_nchw[0] = 0.000000e+00f; + conv2d_nchw[1] = 0.000000e+00f; + conv2d_nchw[2] = 0.000000e+00f; + conv2d_nchw[3] = 0.000000e+00f; + conv2d_nchw[4] = 0.000000e+00f; + conv2d_nchw[5] = 0.000000e+00f; + conv2d_nchw[6] = 0.000000e+00f; + conv2d_nchw[7] = 0.000000e+00f; + conv2d_nchw[8] = 0.000000e+00f; + conv2d_nchw[9] = 0.000000e+00f; + conv2d_nchw[10] = 0.000000e+00f; + conv2d_nchw[11] = 0.000000e+00f; + conv2d_nchw[12] = 0.000000e+00f; + conv2d_nchw[13] = 0.000000e+00f; + for (int rc_outer_outer = 0; rc_outer_outer < 64; ++rc_outer_outer) { + for (int ry_outer_outer = 0; ry_outer_outer < 3; ++ry_outer_outer) { + __syncthreads(); + if (((int)threadIdx.x) < 18) { + pad_temp_shared[(((int)threadIdx.x) * 4)] = (((((1 <= (ry_outer_outer + (((int)blockIdx.x) % 7))) && ((ry_outer_outer + (((int)blockIdx.x) % 7)) < 8)) && (1 <= ((((int)threadIdx.x) * 4) % 9))) && (((((int)threadIdx.x) * 4) % 9) < 8)) ? data[((((((rc_outer_outer * 392) + (((((int)threadIdx.x) * 4) / 9) * 49)) + (ry_outer_outer * 7)) + ((((int)blockIdx.x) % 7) * 7)) + ((((int)threadIdx.x) * 4) % 9)) - 8)] : 0.000000e+00f); + } + if (((int)threadIdx.x) < 18) { + pad_temp_shared[((((int)threadIdx.x) * 4) + 1)] = (((((1 <= (ry_outer_outer + (((int)blockIdx.x) % 7))) && ((ry_outer_outer + (((int)blockIdx.x) % 7)) < 8)) && (1 <= (((((int)threadIdx.x) * 4) + 1) % 9))) && ((((((int)threadIdx.x) * 4) + 1) % 9) < 8)) ? data[((((((rc_outer_outer * 392) + ((((((int)threadIdx.x) * 4) + 1) / 9) * 49)) + (ry_outer_outer * 7)) + ((((int)blockIdx.x) % 7) * 7)) + (((((int)threadIdx.x) * 4) + 1) % 9)) - 8)] : 0.000000e+00f); + } + if (((int)threadIdx.x) < 18) { + pad_temp_shared[((((int)threadIdx.x) * 4) + 2)] = (((((1 <= (ry_outer_outer + (((int)blockIdx.x) % 7))) && ((ry_outer_outer + (((int)blockIdx.x) % 7)) < 8)) && (1 <= (((((int)threadIdx.x) * 4) + 2) % 9))) && ((((((int)threadIdx.x) * 4) + 2) % 9) < 8)) ? data[((((((rc_outer_outer * 392) + ((((((int)threadIdx.x) * 4) + 2) / 9) * 49)) + (ry_outer_outer * 7)) + ((((int)blockIdx.x) % 7) * 7)) + (((((int)threadIdx.x) * 4) + 2) % 9)) - 8)] : 0.000000e+00f); + } + if (((int)threadIdx.x) < 18) { + pad_temp_shared[((((int)threadIdx.x) * 4) + 3)] = (((((1 <= (ry_outer_outer + (((int)blockIdx.x) % 7))) && ((ry_outer_outer + (((int)blockIdx.x) % 7)) < 8)) && (1 <= (((((int)threadIdx.x) * 4) + 3) % 9))) && ((((((int)threadIdx.x) * 4) + 3) % 9) < 8)) ? data[((((((rc_outer_outer * 392) + ((((((int)threadIdx.x) * 4) + 3) / 9) * 49)) + (ry_outer_outer * 7)) + ((((int)blockIdx.x) % 7) * 7)) + (((((int)threadIdx.x) * 4) + 3) % 9)) - 8)] : 0.000000e+00f); + } + kernel_shared[((int)threadIdx.x)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + ((((int)threadIdx.x) / 24) * 4608)) + (rc_outer_outer * 72)) + (((((int)threadIdx.x) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + (((int)threadIdx.x) % 3))]; + kernel_shared[(((int)threadIdx.x) + 64)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 64) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 16) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 1) % 3))]; + kernel_shared[(((int)threadIdx.x) + 128)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 128) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 8) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 2) % 3))]; + kernel_shared[(((int)threadIdx.x) + 192)] = kernel[((((((((((int)blockIdx.x) / 7) * 589824) + ((((int)threadIdx.x) / 24) * 4608)) + (rc_outer_outer * 72)) + (((((int)threadIdx.x) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + (((int)threadIdx.x) % 3)) + 36864)]; + kernel_shared[(((int)threadIdx.x) + 256)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 256) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 16) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 1) % 3))]; + kernel_shared[(((int)threadIdx.x) + 320)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 320) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 8) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 2) % 3))]; + kernel_shared[(((int)threadIdx.x) + 384)] = kernel[((((((((((int)blockIdx.x) / 7) * 589824) + ((((int)threadIdx.x) / 24) * 4608)) + (rc_outer_outer * 72)) + (((((int)threadIdx.x) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + (((int)threadIdx.x) % 3)) + 73728)]; + kernel_shared[(((int)threadIdx.x) + 448)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 448) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 16) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 1) % 3))]; + kernel_shared[(((int)threadIdx.x) + 512)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 512) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 8) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 2) % 3))]; + kernel_shared[(((int)threadIdx.x) + 576)] = kernel[((((((((((int)blockIdx.x) / 7) * 589824) + ((((int)threadIdx.x) / 24) * 4608)) + (rc_outer_outer * 72)) + (((((int)threadIdx.x) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + (((int)threadIdx.x) % 3)) + 110592)]; + kernel_shared[(((int)threadIdx.x) + 640)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 640) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 16) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 1) % 3))]; + kernel_shared[(((int)threadIdx.x) + 704)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 704) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 8) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 2) % 3))]; + kernel_shared[(((int)threadIdx.x) + 768)] = kernel[((((((((((int)blockIdx.x) / 7) * 589824) + ((((int)threadIdx.x) / 24) * 4608)) + (rc_outer_outer * 72)) + (((((int)threadIdx.x) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + (((int)threadIdx.x) % 3)) + 147456)]; + kernel_shared[(((int)threadIdx.x) + 832)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 832) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 16) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 1) % 3))]; + kernel_shared[(((int)threadIdx.x) + 896)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 896) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 8) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 2) % 3))]; + kernel_shared[(((int)threadIdx.x) + 960)] = kernel[((((((((((int)blockIdx.x) / 7) * 589824) + ((((int)threadIdx.x) / 24) * 4608)) + (rc_outer_outer * 72)) + (((((int)threadIdx.x) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + (((int)threadIdx.x) % 3)) + 184320)]; + kernel_shared[(((int)threadIdx.x) + 1024)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 1024) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 16) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 1) % 3))]; + kernel_shared[(((int)threadIdx.x) + 1088)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 1088) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 8) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 2) % 3))]; + kernel_shared[(((int)threadIdx.x) + 1152)] = kernel[((((((((((int)blockIdx.x) / 7) * 589824) + ((((int)threadIdx.x) / 24) * 4608)) + (rc_outer_outer * 72)) + (((((int)threadIdx.x) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + (((int)threadIdx.x) % 3)) + 221184)]; + kernel_shared[(((int)threadIdx.x) + 1216)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 1216) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 16) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 1) % 3))]; + kernel_shared[(((int)threadIdx.x) + 1280)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 1280) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 8) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 2) % 3))]; + kernel_shared[(((int)threadIdx.x) + 1344)] = kernel[((((((((((int)blockIdx.x) / 7) * 589824) + ((((int)threadIdx.x) / 24) * 4608)) + (rc_outer_outer * 72)) + (((((int)threadIdx.x) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + (((int)threadIdx.x) % 3)) + 258048)]; + kernel_shared[(((int)threadIdx.x) + 1408)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 1408) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 16) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 1) % 3))]; + kernel_shared[(((int)threadIdx.x) + 1472)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 1472) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 8) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 2) % 3))]; + kernel_shared[(((int)threadIdx.x) + 1536)] = kernel[((((((((((int)blockIdx.x) / 7) * 589824) + ((((int)threadIdx.x) / 24) * 4608)) + (rc_outer_outer * 72)) + (((((int)threadIdx.x) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + (((int)threadIdx.x) % 3)) + 294912)]; + kernel_shared[(((int)threadIdx.x) + 1600)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 1600) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 16) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 1) % 3))]; + kernel_shared[(((int)threadIdx.x) + 1664)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 1664) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 8) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 2) % 3))]; + kernel_shared[(((int)threadIdx.x) + 1728)] = kernel[((((((((((int)blockIdx.x) / 7) * 589824) + ((((int)threadIdx.x) / 24) * 4608)) + (rc_outer_outer * 72)) + (((((int)threadIdx.x) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + (((int)threadIdx.x) % 3)) + 331776)]; + kernel_shared[(((int)threadIdx.x) + 1792)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 1792) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 16) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 1) % 3))]; + kernel_shared[(((int)threadIdx.x) + 1856)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 1856) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 8) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 2) % 3))]; + kernel_shared[(((int)threadIdx.x) + 1920)] = kernel[((((((((((int)blockIdx.x) / 7) * 589824) + ((((int)threadIdx.x) / 24) * 4608)) + (rc_outer_outer * 72)) + (((((int)threadIdx.x) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + (((int)threadIdx.x) % 3)) + 368640)]; + kernel_shared[(((int)threadIdx.x) + 1984)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 1984) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 16) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 1) % 3))]; + kernel_shared[(((int)threadIdx.x) + 2048)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 2048) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 8) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 2) % 3))]; + kernel_shared[(((int)threadIdx.x) + 2112)] = kernel[((((((((((int)blockIdx.x) / 7) * 589824) + ((((int)threadIdx.x) / 24) * 4608)) + (rc_outer_outer * 72)) + (((((int)threadIdx.x) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + (((int)threadIdx.x) % 3)) + 405504)]; + kernel_shared[(((int)threadIdx.x) + 2176)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 2176) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 16) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 1) % 3))]; + kernel_shared[(((int)threadIdx.x) + 2240)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 2240) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 8) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 2) % 3))]; + kernel_shared[(((int)threadIdx.x) + 2304)] = kernel[((((((((((int)blockIdx.x) / 7) * 589824) + ((((int)threadIdx.x) / 24) * 4608)) + (rc_outer_outer * 72)) + (((((int)threadIdx.x) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + (((int)threadIdx.x) % 3)) + 442368)]; + kernel_shared[(((int)threadIdx.x) + 2368)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 2368) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 16) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 1) % 3))]; + kernel_shared[(((int)threadIdx.x) + 2432)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 2432) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 8) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 2) % 3))]; + kernel_shared[(((int)threadIdx.x) + 2496)] = kernel[((((((((((int)blockIdx.x) / 7) * 589824) + ((((int)threadIdx.x) / 24) * 4608)) + (rc_outer_outer * 72)) + (((((int)threadIdx.x) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + (((int)threadIdx.x) % 3)) + 479232)]; + kernel_shared[(((int)threadIdx.x) + 2560)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 2560) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 16) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 1) % 3))]; + kernel_shared[(((int)threadIdx.x) + 2624)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 2624) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 8) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 2) % 3))]; + kernel_shared[(((int)threadIdx.x) + 2688)] = kernel[((((((((((int)blockIdx.x) / 7) * 589824) + ((((int)threadIdx.x) / 24) * 4608)) + (rc_outer_outer * 72)) + (((((int)threadIdx.x) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + (((int)threadIdx.x) % 3)) + 516096)]; + kernel_shared[(((int)threadIdx.x) + 2752)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 2752) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 16) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 1) % 3))]; + kernel_shared[(((int)threadIdx.x) + 2816)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 2816) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 8) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 2) % 3))]; + kernel_shared[(((int)threadIdx.x) + 2880)] = kernel[((((((((((int)blockIdx.x) / 7) * 589824) + ((((int)threadIdx.x) / 24) * 4608)) + (rc_outer_outer * 72)) + (((((int)threadIdx.x) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + (((int)threadIdx.x) % 3)) + 552960)]; + kernel_shared[(((int)threadIdx.x) + 2944)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 2944) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 16) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 1) % 3))]; + kernel_shared[(((int)threadIdx.x) + 3008)] = kernel[(((((((((int)blockIdx.x) / 7) * 589824) + (((((int)threadIdx.x) + 3008) / 24) * 4608)) + (rc_outer_outer * 72)) + ((((((int)threadIdx.x) + 8) % 24) / 3) * 9)) + (ry_outer_outer * 3)) + ((((int)threadIdx.x) + 2) % 3))]; + __syncthreads(); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[0] * kernel_shared[(((int)threadIdx.x) * 48)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[9] * kernel_shared[((((int)threadIdx.x) * 48) + 3)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[1] * kernel_shared[(((int)threadIdx.x) * 48)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[10] * kernel_shared[((((int)threadIdx.x) * 48) + 3)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[2] * kernel_shared[(((int)threadIdx.x) * 48)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[11] * kernel_shared[((((int)threadIdx.x) * 48) + 3)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[3] * kernel_shared[(((int)threadIdx.x) * 48)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[12] * kernel_shared[((((int)threadIdx.x) * 48) + 3)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[4] * kernel_shared[(((int)threadIdx.x) * 48)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[13] * kernel_shared[((((int)threadIdx.x) * 48) + 3)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[5] * kernel_shared[(((int)threadIdx.x) * 48)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[14] * kernel_shared[((((int)threadIdx.x) * 48) + 3)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[6] * kernel_shared[(((int)threadIdx.x) * 48)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[15] * kernel_shared[((((int)threadIdx.x) * 48) + 3)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[0] * kernel_shared[((((int)threadIdx.x) * 48) + 24)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[9] * kernel_shared[((((int)threadIdx.x) * 48) + 27)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[1] * kernel_shared[((((int)threadIdx.x) * 48) + 24)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[10] * kernel_shared[((((int)threadIdx.x) * 48) + 27)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[2] * kernel_shared[((((int)threadIdx.x) * 48) + 24)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[11] * kernel_shared[((((int)threadIdx.x) * 48) + 27)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[3] * kernel_shared[((((int)threadIdx.x) * 48) + 24)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[12] * kernel_shared[((((int)threadIdx.x) * 48) + 27)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[4] * kernel_shared[((((int)threadIdx.x) * 48) + 24)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[13] * kernel_shared[((((int)threadIdx.x) * 48) + 27)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[5] * kernel_shared[((((int)threadIdx.x) * 48) + 24)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[14] * kernel_shared[((((int)threadIdx.x) * 48) + 27)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[6] * kernel_shared[((((int)threadIdx.x) * 48) + 24)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[15] * kernel_shared[((((int)threadIdx.x) * 48) + 27)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[1] * kernel_shared[((((int)threadIdx.x) * 48) + 1)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[10] * kernel_shared[((((int)threadIdx.x) * 48) + 4)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[2] * kernel_shared[((((int)threadIdx.x) * 48) + 1)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[11] * kernel_shared[((((int)threadIdx.x) * 48) + 4)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[3] * kernel_shared[((((int)threadIdx.x) * 48) + 1)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[12] * kernel_shared[((((int)threadIdx.x) * 48) + 4)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[4] * kernel_shared[((((int)threadIdx.x) * 48) + 1)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[13] * kernel_shared[((((int)threadIdx.x) * 48) + 4)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[5] * kernel_shared[((((int)threadIdx.x) * 48) + 1)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[14] * kernel_shared[((((int)threadIdx.x) * 48) + 4)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[6] * kernel_shared[((((int)threadIdx.x) * 48) + 1)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[15] * kernel_shared[((((int)threadIdx.x) * 48) + 4)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[7] * kernel_shared[((((int)threadIdx.x) * 48) + 1)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[16] * kernel_shared[((((int)threadIdx.x) * 48) + 4)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[1] * kernel_shared[((((int)threadIdx.x) * 48) + 25)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[10] * kernel_shared[((((int)threadIdx.x) * 48) + 28)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[2] * kernel_shared[((((int)threadIdx.x) * 48) + 25)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[11] * kernel_shared[((((int)threadIdx.x) * 48) + 28)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[3] * kernel_shared[((((int)threadIdx.x) * 48) + 25)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[12] * kernel_shared[((((int)threadIdx.x) * 48) + 28)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[4] * kernel_shared[((((int)threadIdx.x) * 48) + 25)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[13] * kernel_shared[((((int)threadIdx.x) * 48) + 28)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[5] * kernel_shared[((((int)threadIdx.x) * 48) + 25)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[14] * kernel_shared[((((int)threadIdx.x) * 48) + 28)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[6] * kernel_shared[((((int)threadIdx.x) * 48) + 25)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[15] * kernel_shared[((((int)threadIdx.x) * 48) + 28)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[7] * kernel_shared[((((int)threadIdx.x) * 48) + 25)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[16] * kernel_shared[((((int)threadIdx.x) * 48) + 28)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[2] * kernel_shared[((((int)threadIdx.x) * 48) + 2)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[11] * kernel_shared[((((int)threadIdx.x) * 48) + 5)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[3] * kernel_shared[((((int)threadIdx.x) * 48) + 2)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[12] * kernel_shared[((((int)threadIdx.x) * 48) + 5)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[4] * kernel_shared[((((int)threadIdx.x) * 48) + 2)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[13] * kernel_shared[((((int)threadIdx.x) * 48) + 5)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[5] * kernel_shared[((((int)threadIdx.x) * 48) + 2)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[14] * kernel_shared[((((int)threadIdx.x) * 48) + 5)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[6] * kernel_shared[((((int)threadIdx.x) * 48) + 2)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[15] * kernel_shared[((((int)threadIdx.x) * 48) + 5)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[7] * kernel_shared[((((int)threadIdx.x) * 48) + 2)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[16] * kernel_shared[((((int)threadIdx.x) * 48) + 5)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[8] * kernel_shared[((((int)threadIdx.x) * 48) + 2)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[17] * kernel_shared[((((int)threadIdx.x) * 48) + 5)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[2] * kernel_shared[((((int)threadIdx.x) * 48) + 26)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[11] * kernel_shared[((((int)threadIdx.x) * 48) + 29)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[3] * kernel_shared[((((int)threadIdx.x) * 48) + 26)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[12] * kernel_shared[((((int)threadIdx.x) * 48) + 29)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[4] * kernel_shared[((((int)threadIdx.x) * 48) + 26)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[13] * kernel_shared[((((int)threadIdx.x) * 48) + 29)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[5] * kernel_shared[((((int)threadIdx.x) * 48) + 26)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[14] * kernel_shared[((((int)threadIdx.x) * 48) + 29)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[6] * kernel_shared[((((int)threadIdx.x) * 48) + 26)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[15] * kernel_shared[((((int)threadIdx.x) * 48) + 29)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[7] * kernel_shared[((((int)threadIdx.x) * 48) + 26)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[16] * kernel_shared[((((int)threadIdx.x) * 48) + 29)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[8] * kernel_shared[((((int)threadIdx.x) * 48) + 26)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[17] * kernel_shared[((((int)threadIdx.x) * 48) + 29)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[18] * kernel_shared[((((int)threadIdx.x) * 48) + 6)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[27] * kernel_shared[((((int)threadIdx.x) * 48) + 9)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[19] * kernel_shared[((((int)threadIdx.x) * 48) + 6)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[28] * kernel_shared[((((int)threadIdx.x) * 48) + 9)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[20] * kernel_shared[((((int)threadIdx.x) * 48) + 6)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[29] * kernel_shared[((((int)threadIdx.x) * 48) + 9)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[21] * kernel_shared[((((int)threadIdx.x) * 48) + 6)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[30] * kernel_shared[((((int)threadIdx.x) * 48) + 9)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[22] * kernel_shared[((((int)threadIdx.x) * 48) + 6)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[31] * kernel_shared[((((int)threadIdx.x) * 48) + 9)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[23] * kernel_shared[((((int)threadIdx.x) * 48) + 6)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[32] * kernel_shared[((((int)threadIdx.x) * 48) + 9)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[24] * kernel_shared[((((int)threadIdx.x) * 48) + 6)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[33] * kernel_shared[((((int)threadIdx.x) * 48) + 9)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[18] * kernel_shared[((((int)threadIdx.x) * 48) + 30)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[27] * kernel_shared[((((int)threadIdx.x) * 48) + 33)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[19] * kernel_shared[((((int)threadIdx.x) * 48) + 30)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[28] * kernel_shared[((((int)threadIdx.x) * 48) + 33)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[20] * kernel_shared[((((int)threadIdx.x) * 48) + 30)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[29] * kernel_shared[((((int)threadIdx.x) * 48) + 33)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[21] * kernel_shared[((((int)threadIdx.x) * 48) + 30)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[30] * kernel_shared[((((int)threadIdx.x) * 48) + 33)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[22] * kernel_shared[((((int)threadIdx.x) * 48) + 30)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[31] * kernel_shared[((((int)threadIdx.x) * 48) + 33)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[23] * kernel_shared[((((int)threadIdx.x) * 48) + 30)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[32] * kernel_shared[((((int)threadIdx.x) * 48) + 33)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[24] * kernel_shared[((((int)threadIdx.x) * 48) + 30)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[33] * kernel_shared[((((int)threadIdx.x) * 48) + 33)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[19] * kernel_shared[((((int)threadIdx.x) * 48) + 7)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[28] * kernel_shared[((((int)threadIdx.x) * 48) + 10)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[20] * kernel_shared[((((int)threadIdx.x) * 48) + 7)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[29] * kernel_shared[((((int)threadIdx.x) * 48) + 10)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[21] * kernel_shared[((((int)threadIdx.x) * 48) + 7)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[30] * kernel_shared[((((int)threadIdx.x) * 48) + 10)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[22] * kernel_shared[((((int)threadIdx.x) * 48) + 7)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[31] * kernel_shared[((((int)threadIdx.x) * 48) + 10)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[23] * kernel_shared[((((int)threadIdx.x) * 48) + 7)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[32] * kernel_shared[((((int)threadIdx.x) * 48) + 10)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[24] * kernel_shared[((((int)threadIdx.x) * 48) + 7)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[33] * kernel_shared[((((int)threadIdx.x) * 48) + 10)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[25] * kernel_shared[((((int)threadIdx.x) * 48) + 7)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[34] * kernel_shared[((((int)threadIdx.x) * 48) + 10)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[19] * kernel_shared[((((int)threadIdx.x) * 48) + 31)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[28] * kernel_shared[((((int)threadIdx.x) * 48) + 34)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[20] * kernel_shared[((((int)threadIdx.x) * 48) + 31)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[29] * kernel_shared[((((int)threadIdx.x) * 48) + 34)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[21] * kernel_shared[((((int)threadIdx.x) * 48) + 31)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[30] * kernel_shared[((((int)threadIdx.x) * 48) + 34)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[22] * kernel_shared[((((int)threadIdx.x) * 48) + 31)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[31] * kernel_shared[((((int)threadIdx.x) * 48) + 34)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[23] * kernel_shared[((((int)threadIdx.x) * 48) + 31)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[32] * kernel_shared[((((int)threadIdx.x) * 48) + 34)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[24] * kernel_shared[((((int)threadIdx.x) * 48) + 31)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[33] * kernel_shared[((((int)threadIdx.x) * 48) + 34)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[25] * kernel_shared[((((int)threadIdx.x) * 48) + 31)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[34] * kernel_shared[((((int)threadIdx.x) * 48) + 34)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[20] * kernel_shared[((((int)threadIdx.x) * 48) + 8)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[29] * kernel_shared[((((int)threadIdx.x) * 48) + 11)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[21] * kernel_shared[((((int)threadIdx.x) * 48) + 8)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[30] * kernel_shared[((((int)threadIdx.x) * 48) + 11)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[22] * kernel_shared[((((int)threadIdx.x) * 48) + 8)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[31] * kernel_shared[((((int)threadIdx.x) * 48) + 11)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[23] * kernel_shared[((((int)threadIdx.x) * 48) + 8)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[32] * kernel_shared[((((int)threadIdx.x) * 48) + 11)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[24] * kernel_shared[((((int)threadIdx.x) * 48) + 8)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[33] * kernel_shared[((((int)threadIdx.x) * 48) + 11)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[25] * kernel_shared[((((int)threadIdx.x) * 48) + 8)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[34] * kernel_shared[((((int)threadIdx.x) * 48) + 11)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[26] * kernel_shared[((((int)threadIdx.x) * 48) + 8)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[35] * kernel_shared[((((int)threadIdx.x) * 48) + 11)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[20] * kernel_shared[((((int)threadIdx.x) * 48) + 32)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[29] * kernel_shared[((((int)threadIdx.x) * 48) + 35)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[21] * kernel_shared[((((int)threadIdx.x) * 48) + 32)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[30] * kernel_shared[((((int)threadIdx.x) * 48) + 35)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[22] * kernel_shared[((((int)threadIdx.x) * 48) + 32)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[31] * kernel_shared[((((int)threadIdx.x) * 48) + 35)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[23] * kernel_shared[((((int)threadIdx.x) * 48) + 32)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[32] * kernel_shared[((((int)threadIdx.x) * 48) + 35)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[24] * kernel_shared[((((int)threadIdx.x) * 48) + 32)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[33] * kernel_shared[((((int)threadIdx.x) * 48) + 35)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[25] * kernel_shared[((((int)threadIdx.x) * 48) + 32)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[34] * kernel_shared[((((int)threadIdx.x) * 48) + 35)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[26] * kernel_shared[((((int)threadIdx.x) * 48) + 32)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[35] * kernel_shared[((((int)threadIdx.x) * 48) + 35)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[36] * kernel_shared[((((int)threadIdx.x) * 48) + 12)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[45] * kernel_shared[((((int)threadIdx.x) * 48) + 15)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[37] * kernel_shared[((((int)threadIdx.x) * 48) + 12)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[46] * kernel_shared[((((int)threadIdx.x) * 48) + 15)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[38] * kernel_shared[((((int)threadIdx.x) * 48) + 12)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[47] * kernel_shared[((((int)threadIdx.x) * 48) + 15)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[39] * kernel_shared[((((int)threadIdx.x) * 48) + 12)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[48] * kernel_shared[((((int)threadIdx.x) * 48) + 15)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[40] * kernel_shared[((((int)threadIdx.x) * 48) + 12)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[49] * kernel_shared[((((int)threadIdx.x) * 48) + 15)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[41] * kernel_shared[((((int)threadIdx.x) * 48) + 12)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[50] * kernel_shared[((((int)threadIdx.x) * 48) + 15)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[42] * kernel_shared[((((int)threadIdx.x) * 48) + 12)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[51] * kernel_shared[((((int)threadIdx.x) * 48) + 15)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[36] * kernel_shared[((((int)threadIdx.x) * 48) + 36)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[45] * kernel_shared[((((int)threadIdx.x) * 48) + 39)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[37] * kernel_shared[((((int)threadIdx.x) * 48) + 36)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[46] * kernel_shared[((((int)threadIdx.x) * 48) + 39)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[38] * kernel_shared[((((int)threadIdx.x) * 48) + 36)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[47] * kernel_shared[((((int)threadIdx.x) * 48) + 39)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[39] * kernel_shared[((((int)threadIdx.x) * 48) + 36)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[48] * kernel_shared[((((int)threadIdx.x) * 48) + 39)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[40] * kernel_shared[((((int)threadIdx.x) * 48) + 36)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[49] * kernel_shared[((((int)threadIdx.x) * 48) + 39)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[41] * kernel_shared[((((int)threadIdx.x) * 48) + 36)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[50] * kernel_shared[((((int)threadIdx.x) * 48) + 39)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[42] * kernel_shared[((((int)threadIdx.x) * 48) + 36)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[51] * kernel_shared[((((int)threadIdx.x) * 48) + 39)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[37] * kernel_shared[((((int)threadIdx.x) * 48) + 13)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[46] * kernel_shared[((((int)threadIdx.x) * 48) + 16)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[38] * kernel_shared[((((int)threadIdx.x) * 48) + 13)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[47] * kernel_shared[((((int)threadIdx.x) * 48) + 16)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[39] * kernel_shared[((((int)threadIdx.x) * 48) + 13)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[48] * kernel_shared[((((int)threadIdx.x) * 48) + 16)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[40] * kernel_shared[((((int)threadIdx.x) * 48) + 13)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[49] * kernel_shared[((((int)threadIdx.x) * 48) + 16)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[41] * kernel_shared[((((int)threadIdx.x) * 48) + 13)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[50] * kernel_shared[((((int)threadIdx.x) * 48) + 16)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[42] * kernel_shared[((((int)threadIdx.x) * 48) + 13)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[51] * kernel_shared[((((int)threadIdx.x) * 48) + 16)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[43] * kernel_shared[((((int)threadIdx.x) * 48) + 13)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[52] * kernel_shared[((((int)threadIdx.x) * 48) + 16)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[37] * kernel_shared[((((int)threadIdx.x) * 48) + 37)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[46] * kernel_shared[((((int)threadIdx.x) * 48) + 40)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[38] * kernel_shared[((((int)threadIdx.x) * 48) + 37)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[47] * kernel_shared[((((int)threadIdx.x) * 48) + 40)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[39] * kernel_shared[((((int)threadIdx.x) * 48) + 37)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[48] * kernel_shared[((((int)threadIdx.x) * 48) + 40)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[40] * kernel_shared[((((int)threadIdx.x) * 48) + 37)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[49] * kernel_shared[((((int)threadIdx.x) * 48) + 40)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[41] * kernel_shared[((((int)threadIdx.x) * 48) + 37)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[50] * kernel_shared[((((int)threadIdx.x) * 48) + 40)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[42] * kernel_shared[((((int)threadIdx.x) * 48) + 37)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[51] * kernel_shared[((((int)threadIdx.x) * 48) + 40)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[43] * kernel_shared[((((int)threadIdx.x) * 48) + 37)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[52] * kernel_shared[((((int)threadIdx.x) * 48) + 40)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[38] * kernel_shared[((((int)threadIdx.x) * 48) + 14)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[47] * kernel_shared[((((int)threadIdx.x) * 48) + 17)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[39] * kernel_shared[((((int)threadIdx.x) * 48) + 14)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[48] * kernel_shared[((((int)threadIdx.x) * 48) + 17)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[40] * kernel_shared[((((int)threadIdx.x) * 48) + 14)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[49] * kernel_shared[((((int)threadIdx.x) * 48) + 17)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[41] * kernel_shared[((((int)threadIdx.x) * 48) + 14)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[50] * kernel_shared[((((int)threadIdx.x) * 48) + 17)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[42] * kernel_shared[((((int)threadIdx.x) * 48) + 14)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[51] * kernel_shared[((((int)threadIdx.x) * 48) + 17)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[43] * kernel_shared[((((int)threadIdx.x) * 48) + 14)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[52] * kernel_shared[((((int)threadIdx.x) * 48) + 17)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[44] * kernel_shared[((((int)threadIdx.x) * 48) + 14)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[53] * kernel_shared[((((int)threadIdx.x) * 48) + 17)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[38] * kernel_shared[((((int)threadIdx.x) * 48) + 38)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[47] * kernel_shared[((((int)threadIdx.x) * 48) + 41)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[39] * kernel_shared[((((int)threadIdx.x) * 48) + 38)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[48] * kernel_shared[((((int)threadIdx.x) * 48) + 41)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[40] * kernel_shared[((((int)threadIdx.x) * 48) + 38)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[49] * kernel_shared[((((int)threadIdx.x) * 48) + 41)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[41] * kernel_shared[((((int)threadIdx.x) * 48) + 38)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[50] * kernel_shared[((((int)threadIdx.x) * 48) + 41)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[42] * kernel_shared[((((int)threadIdx.x) * 48) + 38)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[51] * kernel_shared[((((int)threadIdx.x) * 48) + 41)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[43] * kernel_shared[((((int)threadIdx.x) * 48) + 38)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[52] * kernel_shared[((((int)threadIdx.x) * 48) + 41)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[44] * kernel_shared[((((int)threadIdx.x) * 48) + 38)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[53] * kernel_shared[((((int)threadIdx.x) * 48) + 41)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[54] * kernel_shared[((((int)threadIdx.x) * 48) + 18)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[63] * kernel_shared[((((int)threadIdx.x) * 48) + 21)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[55] * kernel_shared[((((int)threadIdx.x) * 48) + 18)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[64] * kernel_shared[((((int)threadIdx.x) * 48) + 21)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[56] * kernel_shared[((((int)threadIdx.x) * 48) + 18)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[65] * kernel_shared[((((int)threadIdx.x) * 48) + 21)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[57] * kernel_shared[((((int)threadIdx.x) * 48) + 18)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[66] * kernel_shared[((((int)threadIdx.x) * 48) + 21)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[58] * kernel_shared[((((int)threadIdx.x) * 48) + 18)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[67] * kernel_shared[((((int)threadIdx.x) * 48) + 21)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[59] * kernel_shared[((((int)threadIdx.x) * 48) + 18)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[68] * kernel_shared[((((int)threadIdx.x) * 48) + 21)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[60] * kernel_shared[((((int)threadIdx.x) * 48) + 18)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[69] * kernel_shared[((((int)threadIdx.x) * 48) + 21)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[54] * kernel_shared[((((int)threadIdx.x) * 48) + 42)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[63] * kernel_shared[((((int)threadIdx.x) * 48) + 45)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[55] * kernel_shared[((((int)threadIdx.x) * 48) + 42)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[64] * kernel_shared[((((int)threadIdx.x) * 48) + 45)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[56] * kernel_shared[((((int)threadIdx.x) * 48) + 42)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[65] * kernel_shared[((((int)threadIdx.x) * 48) + 45)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[57] * kernel_shared[((((int)threadIdx.x) * 48) + 42)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[66] * kernel_shared[((((int)threadIdx.x) * 48) + 45)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[58] * kernel_shared[((((int)threadIdx.x) * 48) + 42)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[67] * kernel_shared[((((int)threadIdx.x) * 48) + 45)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[59] * kernel_shared[((((int)threadIdx.x) * 48) + 42)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[68] * kernel_shared[((((int)threadIdx.x) * 48) + 45)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[60] * kernel_shared[((((int)threadIdx.x) * 48) + 42)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[69] * kernel_shared[((((int)threadIdx.x) * 48) + 45)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[55] * kernel_shared[((((int)threadIdx.x) * 48) + 19)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[64] * kernel_shared[((((int)threadIdx.x) * 48) + 22)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[56] * kernel_shared[((((int)threadIdx.x) * 48) + 19)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[65] * kernel_shared[((((int)threadIdx.x) * 48) + 22)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[57] * kernel_shared[((((int)threadIdx.x) * 48) + 19)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[66] * kernel_shared[((((int)threadIdx.x) * 48) + 22)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[58] * kernel_shared[((((int)threadIdx.x) * 48) + 19)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[67] * kernel_shared[((((int)threadIdx.x) * 48) + 22)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[59] * kernel_shared[((((int)threadIdx.x) * 48) + 19)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[68] * kernel_shared[((((int)threadIdx.x) * 48) + 22)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[60] * kernel_shared[((((int)threadIdx.x) * 48) + 19)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[69] * kernel_shared[((((int)threadIdx.x) * 48) + 22)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[61] * kernel_shared[((((int)threadIdx.x) * 48) + 19)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[70] * kernel_shared[((((int)threadIdx.x) * 48) + 22)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[55] * kernel_shared[((((int)threadIdx.x) * 48) + 43)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[64] * kernel_shared[((((int)threadIdx.x) * 48) + 46)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[56] * kernel_shared[((((int)threadIdx.x) * 48) + 43)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[65] * kernel_shared[((((int)threadIdx.x) * 48) + 46)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[57] * kernel_shared[((((int)threadIdx.x) * 48) + 43)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[66] * kernel_shared[((((int)threadIdx.x) * 48) + 46)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[58] * kernel_shared[((((int)threadIdx.x) * 48) + 43)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[67] * kernel_shared[((((int)threadIdx.x) * 48) + 46)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[59] * kernel_shared[((((int)threadIdx.x) * 48) + 43)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[68] * kernel_shared[((((int)threadIdx.x) * 48) + 46)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[60] * kernel_shared[((((int)threadIdx.x) * 48) + 43)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[69] * kernel_shared[((((int)threadIdx.x) * 48) + 46)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[61] * kernel_shared[((((int)threadIdx.x) * 48) + 43)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[70] * kernel_shared[((((int)threadIdx.x) * 48) + 46)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[56] * kernel_shared[((((int)threadIdx.x) * 48) + 20)])); + conv2d_nchw[0] = (conv2d_nchw[0] + (pad_temp_shared[65] * kernel_shared[((((int)threadIdx.x) * 48) + 23)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[57] * kernel_shared[((((int)threadIdx.x) * 48) + 20)])); + conv2d_nchw[1] = (conv2d_nchw[1] + (pad_temp_shared[66] * kernel_shared[((((int)threadIdx.x) * 48) + 23)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[58] * kernel_shared[((((int)threadIdx.x) * 48) + 20)])); + conv2d_nchw[2] = (conv2d_nchw[2] + (pad_temp_shared[67] * kernel_shared[((((int)threadIdx.x) * 48) + 23)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[59] * kernel_shared[((((int)threadIdx.x) * 48) + 20)])); + conv2d_nchw[3] = (conv2d_nchw[3] + (pad_temp_shared[68] * kernel_shared[((((int)threadIdx.x) * 48) + 23)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[60] * kernel_shared[((((int)threadIdx.x) * 48) + 20)])); + conv2d_nchw[4] = (conv2d_nchw[4] + (pad_temp_shared[69] * kernel_shared[((((int)threadIdx.x) * 48) + 23)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[61] * kernel_shared[((((int)threadIdx.x) * 48) + 20)])); + conv2d_nchw[5] = (conv2d_nchw[5] + (pad_temp_shared[70] * kernel_shared[((((int)threadIdx.x) * 48) + 23)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[62] * kernel_shared[((((int)threadIdx.x) * 48) + 20)])); + conv2d_nchw[6] = (conv2d_nchw[6] + (pad_temp_shared[71] * kernel_shared[((((int)threadIdx.x) * 48) + 23)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[56] * kernel_shared[((((int)threadIdx.x) * 48) + 44)])); + conv2d_nchw[7] = (conv2d_nchw[7] + (pad_temp_shared[65] * kernel_shared[((((int)threadIdx.x) * 48) + 47)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[57] * kernel_shared[((((int)threadIdx.x) * 48) + 44)])); + conv2d_nchw[8] = (conv2d_nchw[8] + (pad_temp_shared[66] * kernel_shared[((((int)threadIdx.x) * 48) + 47)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[58] * kernel_shared[((((int)threadIdx.x) * 48) + 44)])); + conv2d_nchw[9] = (conv2d_nchw[9] + (pad_temp_shared[67] * kernel_shared[((((int)threadIdx.x) * 48) + 47)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[59] * kernel_shared[((((int)threadIdx.x) * 48) + 44)])); + conv2d_nchw[10] = (conv2d_nchw[10] + (pad_temp_shared[68] * kernel_shared[((((int)threadIdx.x) * 48) + 47)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[60] * kernel_shared[((((int)threadIdx.x) * 48) + 44)])); + conv2d_nchw[11] = (conv2d_nchw[11] + (pad_temp_shared[69] * kernel_shared[((((int)threadIdx.x) * 48) + 47)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[61] * kernel_shared[((((int)threadIdx.x) * 48) + 44)])); + conv2d_nchw[12] = (conv2d_nchw[12] + (pad_temp_shared[70] * kernel_shared[((((int)threadIdx.x) * 48) + 47)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[62] * kernel_shared[((((int)threadIdx.x) * 48) + 44)])); + conv2d_nchw[13] = (conv2d_nchw[13] + (pad_temp_shared[71] * kernel_shared[((((int)threadIdx.x) * 48) + 47)])); + } + } + for (int i1_inner = 0; i1_inner < 2; ++i1_inner) { + for (int i3_inner = 0; i3_inner < 7; ++i3_inner) { + compute[((((((((int)blockIdx.x) / 7) * 6272) + (((int)threadIdx.x) * 98)) + (i1_inner * 49)) + ((((int)blockIdx.x) % 7) * 7)) + i3_inner)] = max((conv2d_nchw[((i1_inner * 7) + i3_inner)] + bias[((((((int)blockIdx.x) / 7) * 128) + (((int)threadIdx.x) * 2)) + i1_inner)]), 0.000000e+00f); + } + } +} +``` + +以下是恢复搜索的例子。这种情况下要自行创建搜索策略和 cost 模型,并通过日志文件恢复搜索策略和 cost 模型的状态。以下示例中,我们恢复状态,并多进行 5 次训练。 + +``` python +def resume_search(task, log_file): + print("Resume search:") + cost_model = auto_scheduler.XGBModel() + cost_model.update_from_file(log_file) + search_policy = auto_scheduler.SketchPolicy( + task, cost_model, init_search_callbacks=[auto_scheduler.PreloadMeasuredStates(log_file)] + ) + measure_ctx = auto_scheduler.LocalRPCMeasureContext(min_repeat_ms=300) + tune_option = auto_scheduler.TuningOptions( + num_measure_trials=5, + runner=measure_ctx.runner, + measure_callbacks=[auto_scheduler.RecordToFile(log_file)], + ) + task.tune(tune_option, search_policy=search_policy) + + # 终止测试过程 + del measure_ctx + +resume_search(task, log_file) +``` + +输出结果: + +``` bash +Resume search: +/usr/local/lib/python3.7/dist-packages/xgboost/training.py:17: UserWarning: Old style callback is deprecated. See: https://xgboost.readthedocs.io/en/latest/python/callbacks.html + warnings.warn(f'Old style callback is deprecated. See: {link}', UserWarning) +Get devices for measurement successfully! +``` + +**脚本总运行时长:**( 3 分 12.949 秒) + +[下载 Python 源代码:tune_conv2d_layer_cuda.py](https://tvm.apache.org/docs/_downloads/e3e540f3b477c0c52d8eb73e674e8ffd/tune_conv2d_layer_cuda.py) + +[下载 Jupyter notebook:tune_conv2d_layer_cuda.ipynb](https://tvm.apache.org/docs/_downloads/5f1f7bd7d90710fd404f7bcdc4965622/tune_conv2d_layer_cuda.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/autoscheduler/02-autoschedule_x86.md b/versioned_docs/version-0.12.0/how_to/autoscheduler/02-autoschedule_x86.md new file mode 100644 index 00000000..e9eddfee --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/autoscheduler/02-autoschedule_x86.md @@ -0,0 +1,552 @@ +--- +title: 为 x86 CPU 自动调度神经网络 +--- + +# 为 x86 CPU 自动调度神经网络 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/tune_with_autoscheduler/tune_network_x86.html#sphx-glr-download-how-to-tune-with-autoscheduler-tune-network-x86-py) 下载完整的示例代码 +::: + +**作者**:[Lianmin Zheng](https://github.com/merrymercy),[Chengfan Jia](https://github.com/jcf94/) + +针对特定设备和工作负载自动调优,对于获取最佳性能至关重要。本文介绍如何使用 auto-scheduler 为 x86 CPU 调优整个神经网络。 + +为自动调优神经网络,将网络划分为小的子图并独立调优。每个子图被视为一个搜索任务,任务调度器对时间进行切片并动态地为这些任务分配时间资源,并预测每个任务对端到端执行时间的影响,优先考虑最能减少执行时间的任务。 + +对于每个子图,使用 `tvm/python/topi` 中的计算声明来获取张量表达式形式的计算 DAG。然后使用 auto-scheduler 来构建这个 DAG 的搜索空间并搜索合适的调度(底层优化)。 + +与基于模板的 [AutoTVM](/docs/how_to/autotune)(依赖手动模板来定义搜索空间的) 不同,auto-scheduler 不需要任何调度模板。换言之,auto-scheduler 只使用 `tvm/python/topi` 中的计算声明,不使用现有的调度模板。 + +注意,本教程无法在 Windows 或最新版本的 macOS 上运行。如需运行,请将本教程的主体放在 `if __name__ == "__main__":` 代码块中。 + +``` python +import numpy as np + +import tvm +from tvm import relay, auto_scheduler +from tvm.relay import data_dep_optimization as ddo +import tvm.relay.testing +from tvm.contrib import graph_executor +``` + +## 定义网络 + +首先,要使用 Relay 前端 API 定义网络。可以从 `tvm.relay.testing` 加载一些预定义的网络。也可以从 MXNet、ONNX、PyTorch 和 TensorFlow 加载模型(参见 [前端教程](/docs/how_to/compile) )。 + +对于卷积神经网络,尽管 auto-scheduler 可以在任何布局下正常运行,但通过 NHWC 布局实现的性能最佳。auto-scheduler 对 NHWC 布局进行了很多优化,因此推荐将模型转换为 NHWC 布局,从而得以使用 auto-scheduler。可用 [ConvertLayout](https://tvm.apache.org/docs/arch/convert_layout.html#convert-layout-usage) pass 在 TVM 中进行布局转换。 + +``` python +def get_network(name, batch_size, layout="NHWC", dtype="float32", use_sparse=False): + """Get the symbol definition and random weight of a network""" + + # auto-scheduler 更适合 NHWC 布局 + if layout == "NHWC": + image_shape = (224, 224, 3) + elif layout == "NCHW": + image_shape = (3, 224, 224) + else: + raise ValueError("Invalid layout: " + layout) + + input_shape = (batch_size,) + image_shape + output_shape = (batch_size, 1000) + + if name.startswith("resnet-"): + n_layer = int(name.split("-")[1]) + mod, params = relay.testing.resnet.get_workload( + num_layers=n_layer, + batch_size=batch_size, + layout=layout, + dtype=dtype, + image_shape=image_shape, + ) + elif name.startswith("resnet3d-"): + n_layer = int(name.split("-")[1]) + mod, params = relay.testing.resnet.get_workload( + num_layers=n_layer, + batch_size=batch_size, + layout=layout, + dtype=dtype, + image_shape=image_shape, + ) + elif name == "mobilenet": + mod, params = relay.testing.mobilenet.get_workload( + batch_size=batch_size, layout=layout, dtype=dtype, image_shape=image_shape + ) + elif name == "squeezenet_v1.1": + assert layout == "NCHW", "squeezenet_v1.1 only supports NCHW layout" + mod, params = relay.testing.squeezenet.get_workload( + version="1.1", + batch_size=batch_size, + dtype=dtype, + image_shape=image_shape, + ) + elif name == "inception_v3": + input_shape = (batch_size, 3, 299, 299) if layout == "NCHW" else (batch_size, 299, 299, 3) + mod, params = relay.testing.inception_v3.get_workload(batch_size=batch_size, dtype=dtype) + elif name == "mxnet": + # MXNet 模型的示例 + from mxnet.gluon.model_zoo.vision import get_model + assert layout == "NCHW" + + block = get_model("resnet50_v1", pretrained=True) + mod, params = relay.frontend.from_mxnet(block, shape={"data": input_shape}, dtype=dtype) + net = mod["main"] + net = relay.Function( + net.params, relay.nn.softmax(net.body), None, net.type_params, net.attrs + ) + mod = tvm.IRModule.from_expr(net) + elif name == "mlp": + mod, params = relay.testing.mlp.get_workload( + batch_size=batch_size, dtype=dtype, image_shape=image_shape, num_classes=1000 + ) + else: + raise ValueError("Network not found.") + + if use_sparse: + from tvm.topi.sparse.utils import convert_model_dense_to_sparse + + mod, params = convert_model_dense_to_sparse(mod, params, bs_r=4, random_params=True) + + return mod, params, input_shape, output_shape + +# 定义神经网络和编译 target。 +# 若 target 机器支持 avx512 指令, +# 使用 "llvm -mcpu=skylake-avx512" 替换 "llvm -mcpu=core-avx2" +network = "resnet-50" +use_sparse = False +batch_size = 1 +layout = "NHWC" +target = tvm.target.Target("llvm -mcpu=core-avx2") +dtype = "float32" +log_file = "%s-%s-B%d-%s.json" % (network, layout, batch_size, target.kind.name) +``` + +## 提取搜索任务 + +接下来,从网络中提取搜索任务及其权重。任务的权重是任务的子图在整个网络中出现的次数。通过使用权重,可以将网络的端到端延迟近似为 `sum(latency[t] * weight[t])`,其中 `latency[t]` 是任务的延迟,而 `weight[t]` 是任务的权重,任务调度器仅针对该目标进行优化。 + +``` python +# 从网络中提取任务 +print("Get model...") +mod, params, input_shape, output_shape = get_network( + network, + batch_size, + layout, + dtype=dtype, + use_sparse=use_sparse, +) +print("Extract tasks...") +tasks, task_weights = auto_scheduler.extract_tasks(mod["main"], params, target) + +for idx, task in enumerate(tasks): + print("========== Task %d (workload key: %s) ==========" % (idx, task.workload_key)) + print(task.compute_dag) +``` + +输出结果: + +``` bash +Get model... +Extract tasks... +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +========== Task 0 (workload key: ["8654f16aeddf785bad9f028164b3a48d", [1, 56, 56, 64], [1, 1, 64, 256], [1, 56, 56, 256]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 64] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 64, 256] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) + +========== Task 1 (workload key: ["b9a4f9bd1416ba25810cb3de27628ace", [1, 14, 14, 256], [1, 1, 256, 1024], [1, 14, 14, 1024], [1, 1, 1, 1024], [1, 14, 14, 1024]]) ========== +placeholder = PLACEHOLDER [1, 14, 14, 256] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 256, 1024] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 14, 14, 1024] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, ax1, ax2, ax3]) +placeholder = PLACEHOLDER [1, 1, 1, 1024] +T_add(ax0, ax1, ax2, ax3) = (T_add[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 2 (workload key: ["12cb81d4ad0a81be02dedf09d1ac8391", [1, 14, 14, 256], [1, 1, 256, 1024], [1, 14, 14, 1024], [1, 14, 14, 1024]]) ========== +placeholder = PLACEHOLDER [1, 14, 14, 256] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 256, 1024] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 14, 14, 1024] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, ax1, ax2, ax3]) + +========== Task 3 (workload key: ["56af7508fbcdf6d851892b1e8434667b", [1, 14, 14, 1024], [1, 1, 1024, 512], [1, 1, 1, 512], [1, 7, 7, 512]]) ========== +placeholder = PLACEHOLDER [1, 14, 14, 1024] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 1024, 512] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, ((yy*2) + ry), ((xx*2) + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 512] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 4 (workload key: ["06f578e6519a86e85028eecf4de64b25", [1, 56, 56, 256], [1, 1, 256, 512], [1, 28, 28, 512]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 256] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 256, 512] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, ((yy*2) + ry), ((xx*2) + rx), rc]*placeholder[ry, rx, rc, ff]) + +========== Task 5 (workload key: ["c68f92478eb18145106184c587d212b6", [1, 14, 14, 256], [6, 6, 256, 256], [1, 1, 1, 256], [1, 14, 14, 256]]) ========== +placeholder = PLACEHOLDER [1, 14, 14, 256] +data_pad(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 15)) && (i2 >= 1)) && (i2 < 15)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +input_tile(eps, nu, p, ci) = data_pad[floordiv(p, 16), ((floormod(floordiv(p, 4), 4)*4) + eps), ((floormod(p, 4)*4) + nu), ci] +B(i, j) = select(((floormod(i, 6) == 5) && (floormod(j, 6) == 5)), 1f, select(((floormod(i, 6) == 5) && (floormod(j, 6) == 4)), ..(OMITTED).. (floormod(j, 6) == 1)), 0f, select(((floormod(i, 6) == 0) && (floormod(j, 6) == 0)), 1f, 0f)))))))))))))))))))))))))))))))))))) +data_pack(eps, nu, p, ci) += ((input_tile[r_a, r_b, p, ci]*B[r_a, eps])*B[r_b, nu]) +placeholder = PLACEHOLDER [6, 6, 256, 256] +bgemm(eps, nu, p, co) += (data_pack[eps, nu, p, ci]*placeholder[eps, nu, co, ci]) +A(i, j) = select(((floormod(i, 6) == 5) && (floormod(j, 4) == 3)), 1f, select(((floormod(i, 6) == 5) && (floormod(j, 4) == 2)), ..(OMITTED).. 6) == 0) && (floormod(j, 4) == 1)), 0f, select(((floormod(i, 6) == 0) && (floormod(j, 4) == 0)), 1f, 0f)))))))))))))))))))))))) +inverse(vh, vw, p, co) += ((bgemm[r_a, r_b, p, co]*A[r_a, vh])*A[r_b, vw]) +conv2d_winograd(n, h, w, co) = inverse[floormod(h, 4), floormod(w, 4), ((((n*4)*4) + (floordiv(h, 4)*4)) + floordiv(w, 4)), co] +placeholder = PLACEHOLDER [1, 1, 1, 256] +T_add(ax0, ax1, ax2, ax3) = (conv2d_winograd[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 6 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 14, 14, 1024], [1, 1, 1024, 256], [1, 1, 1, 256], [1, 14, 14, 256]]) ========== +placeholder = PLACEHOLDER [1, 14, 14, 1024] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 1024, 256] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 256] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 7 (workload key: ["12cb81d4ad0a81be02dedf09d1ac8391", [1, 28, 28, 128], [1, 1, 128, 512], [1, 28, 28, 512], [1, 28, 28, 512]]) ========== +placeholder = PLACEHOLDER [1, 28, 28, 128] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 128, 512] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 28, 28, 512] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, ax1, ax2, ax3]) + +========== Task 8 (workload key: ["12cb81d4ad0a81be02dedf09d1ac8391", [1, 56, 56, 64], [1, 1, 64, 256], [1, 56, 56, 256], [1, 56, 56, 256]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 64] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 64, 256] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 56, 56, 256] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, ax1, ax2, ax3]) + +========== Task 9 (workload key: ["ecec634b4882c5731f86cce3109db636", [1, 28, 28, 128], [6, 6, 128, 128], [1, 1, 1, 128], [1, 28, 28, 128]]) ========== +placeholder = PLACEHOLDER [1, 28, 28, 128] +data_pad(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 29)) && (i2 >= 1)) && (i2 < 29)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +input_tile(eps, nu, p, ci) = data_pad[floordiv(p, 49), ((floormod(floordiv(p, 7), 7)*4) + eps), ((floormod(p, 7)*4) + nu), ci] +B(i, j) = select(((floormod(i, 6) == 5) && (floormod(j, 6) == 5)), 1f, select(((floormod(i, 6) == 5) && (floormod(j, 6) == 4)), ..(OMITTED).. (floormod(j, 6) == 1)), 0f, select(((floormod(i, 6) == 0) && (floormod(j, 6) == 0)), 1f, 0f)))))))))))))))))))))))))))))))))))) +data_pack(eps, nu, p, ci) += ((input_tile[r_a, r_b, p, ci]*B[r_a, eps])*B[r_b, nu]) +placeholder = PLACEHOLDER [6, 6, 128, 128] +bgemm(eps, nu, p, co) += (data_pack[eps, nu, p, ci]*placeholder[eps, nu, co, ci]) +A(i, j) = select(((floormod(i, 6) == 5) && (floormod(j, 4) == 3)), 1f, select(((floormod(i, 6) == 5) && (floormod(j, 4) == 2)), ..(OMITTED).. 6) == 0) && (floormod(j, 4) == 1)), 0f, select(((floormod(i, 6) == 0) && (floormod(j, 4) == 0)), 1f, 0f)))))))))))))))))))))))) +inverse(vh, vw, p, co) += ((bgemm[r_a, r_b, p, co]*A[r_a, vh])*A[r_b, vw]) +conv2d_winograd(n, h, w, co) = inverse[floormod(h, 4), floormod(w, 4), ((((n*7)*7) + (floordiv(h, 4)*7)) + floordiv(w, 4)), co] +placeholder = PLACEHOLDER [1, 1, 1, 128] +T_add(ax0, ax1, ax2, ax3) = (conv2d_winograd[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 10 (workload key: ["d7b65649a4dd54becea0a52aabbc5af5", [1, 1000], [1, 1000]]) ========== +placeholder = PLACEHOLDER [1, 1000] +T_softmax_maxelem(i0) max= placeholder[i0, k] +T_softmax_exp(i0, i1) = tir.exp((placeholder[i0, i1] - T_softmax_maxelem[i0])) +T_softmax_expsum(i0) += T_softmax_exp[i0, k] +T_softmax_norm(i0, i1) = (T_softmax_exp[i0, i1]/T_softmax_expsum[i0]) + +========== Task 11 (workload key: ["69115f188984ae34ede37c3b8ca40b43", [1, 7, 7, 2048], [1, 1, 1, 2048]]) ========== +placeholder = PLACEHOLDER [1, 7, 7, 2048] +tensor(ax0, ax1, ax2, ax3) += placeholder[ax0, ((ax1*7) + rv0), ((ax2*7) + rv1), ax3] +tensor(ax0, ax1, ax2, ax3) = (tensor[ax0, ax1, ax2, ax3]/(float32((select((bool)1, ((ax1 + 1)*7), (((ax1 + 1)*7) + 1)) - (ax1*7)))*float32((select((bool)1, ((ax2 + 1)*7), (((ax2 + 1)*7) + 1)) - (ax2*7))))) + +========== Task 12 (workload key: ["12cb81d4ad0a81be02dedf09d1ac8391", [1, 7, 7, 512], [1, 1, 512, 2048], [1, 7, 7, 2048], [1, 7, 7, 2048]]) ========== +placeholder = PLACEHOLDER [1, 7, 7, 512] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 512, 2048] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 7, 7, 2048] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, ax1, ax2, ax3]) + +========== Task 13 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 28, 28, 512], [1, 1, 512, 128], [1, 1, 1, 128], [1, 28, 28, 128]]) ========== +placeholder = PLACEHOLDER [1, 28, 28, 512] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 512, 128] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 128] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 14 (workload key: ["56af7508fbcdf6d851892b1e8434667b", [1, 28, 28, 512], [1, 1, 512, 256], [1, 1, 1, 256], [1, 14, 14, 256]]) ========== +placeholder = PLACEHOLDER [1, 28, 28, 512] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 512, 256] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, ((yy*2) + ry), ((xx*2) + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 256] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 15 (workload key: ["06f578e6519a86e85028eecf4de64b25", [1, 28, 28, 512], [1, 1, 512, 1024], [1, 14, 14, 1024]]) ========== +placeholder = PLACEHOLDER [1, 28, 28, 512] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 512, 1024] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, ((yy*2) + ry), ((xx*2) + rx), rc]*placeholder[ry, rx, rc, ff]) + +========== Task 16 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 7, 7, 2048], [1, 1, 2048, 512], [1, 1, 1, 512], [1, 7, 7, 512]]) ========== +placeholder = PLACEHOLDER [1, 7, 7, 2048] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 2048, 512] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 512] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 17 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 56, 56, 256], [1, 1, 256, 64], [1, 1, 1, 64], [1, 56, 56, 64]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 256] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 256, 64] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 64] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 18 (workload key: ["b9a4f9bd1416ba25810cb3de27628ace", [1, 56, 56, 64], [1, 1, 64, 256], [1, 56, 56, 256], [1, 1, 1, 256], [1, 56, 56, 256]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 64] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 64, 256] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 56, 56, 256] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, ax1, ax2, ax3]) +placeholder = PLACEHOLDER [1, 1, 1, 256] +T_add(ax0, ax1, ax2, ax3) = (T_add[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 19 (workload key: ["b9a4f9bd1416ba25810cb3de27628ace", [1, 28, 28, 128], [1, 1, 128, 512], [1, 28, 28, 512], [1, 1, 1, 512], [1, 28, 28, 512]]) ========== +placeholder = PLACEHOLDER [1, 28, 28, 128] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 128, 512] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 28, 28, 512] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, ax1, ax2, ax3]) +placeholder = PLACEHOLDER [1, 1, 1, 512] +T_add(ax0, ax1, ax2, ax3) = (T_add[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 20 (workload key: ["86551f1a74663d3ceafd5884659d3478", [1, 7, 7, 512], [3, 3, 512, 512], [1, 1, 1, 512], [1, 7, 7, 512]]) ========== +placeholder = PLACEHOLDER [1, 7, 7, 512] +pad_temp(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 8)) && (i2 >= 1)) && (i2 < 8)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 512, 512] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 512] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 21 (workload key: ["86551f1a74663d3ceafd5884659d3478", [1, 56, 56, 64], [3, 3, 64, 64], [1, 1, 1, 64], [1, 56, 56, 64]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 64] +pad_temp(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 57)) && (i2 >= 1)) && (i2 < 57)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 64, 64] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 64] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 22 (workload key: ["56af7508fbcdf6d851892b1e8434667b", [1, 56, 56, 256], [1, 1, 256, 128], [1, 1, 1, 128], [1, 28, 28, 128]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 256] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 256, 128] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, ((yy*2) + ry), ((xx*2) + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 128] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 23 (workload key: ["ff5ea7f814e5c497bb685e7385cf7159", [1, 7, 7, 512], [1, 1, 512, 2048], [1, 7, 7, 2048], [1, 1, 1, 2048], [1, 1, 1, 2048], [1, 7, 7, 2048]]) ========== +placeholder = PLACEHOLDER [1, 7, 7, 512] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 512, 2048] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 7, 7, 2048] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, ax1, ax2, ax3]) +placeholder = PLACEHOLDER [1, 1, 1, 2048] +T_multiply(ax0, ax1, ax2, ax3) = (T_add[ax0, ax1, ax2, ax3]*placeholder[ax0, 0, 0, ax3]) +placeholder = PLACEHOLDER [1, 1, 1, 2048] +T_add(ax0, ax1, ax2, ax3) = (T_multiply[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 24 (workload key: ["96daaa9daa1b41bc383b7c05ce8b58de", [1, 224, 224, 3], [7, 7, 3, 64], [1, 1, 1, 64], [1, 112, 112, 64]]) ========== +placeholder = PLACEHOLDER [1, 224, 224, 3] +pad_temp(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 3) && (i1 < 227)) && (i2 >= 3)) && (i2 < 227)), placeholder[i0, (i1 - 3), (i2 - 3), i3], 0f) +placeholder = PLACEHOLDER [7, 7, 3, 64] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, ((yy*2) + ry), ((xx*2) + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 64] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 25 (workload key: ["06f578e6519a86e85028eecf4de64b25", [1, 14, 14, 1024], [1, 1, 1024, 2048], [1, 7, 7, 2048]]) ========== +placeholder = PLACEHOLDER [1, 14, 14, 1024] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 1024, 2048] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, ((yy*2) + ry), ((xx*2) + rx), rc]*placeholder[ry, rx, rc, ff]) + +========== Task 26 (workload key: ["7d44c6e3c81cd80f61ff2265b2bae89a", [1, 2048], [1000, 2048], [1, 1000], [1, 1000]]) ========== +placeholder = PLACEHOLDER [1, 2048] +placeholder = PLACEHOLDER [1000, 2048] +T_matmul_NT(i, j) += (placeholder[i, k]*placeholder[j, k]) +placeholder = PLACEHOLDER [1, 1000] +T_add(ax0, ax1) = (T_matmul_NT[ax0, ax1] + placeholder[ax0, ax1]) + +========== Task 27 (workload key: ["64b98c71af70a904fdbb81d7d4188d84", [1, 112, 112, 64], [1, 1, 1, 64], [1, 56, 56, 64]]) ========== +placeholder = PLACEHOLDER [1, 112, 112, 64] +pad_temp(ax0, ax1, ax2, ax3) = tir.if_then_else(((((ax1 >= 1) && (ax1 < 113)) && (ax2 >= 1)) && (ax2 < 113)), placeholder[ax0, (ax1 - 1), (ax2 - 1), ax3], -3.40282e+38f) +tensor(ax0, ax1, ax2, ax3) max= pad_temp[ax0, ((ax1*2) + rv0), ((ax2*2) + rv1), ax3] +placeholder = PLACEHOLDER [1, 1, 1, 64] +T_add(ax0, ax1, ax2, ax3) = (tensor[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 28 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 56, 56, 64], [1, 1, 64, 64], [1, 1, 1, 64], [1, 56, 56, 64]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 64] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 64, 64] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 64] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) +``` + +## 开始调优 + +接下来为调优和启动搜索任务设置一些选项 + +* `num_measure_trials` 是调优期间可以使用的测试次数(根据自己的时间预算调整这个参数),若要进行快速演示,可将其设置为较小的数字(例如 200)。推荐将其设置为 `800 * len(tasks)` 左右,以便使搜索收敛。比如 ResNet-50 有 29 个任务,所以可以设置为 20000。 +* 此外,使用 `RecordToFile` 将测试记录转储到日志文件中,测试记录可用于历史最佳查询、恢复搜索以及进行后续分析。 +* 更多参数参见 `auto_scheduler.TuningOptions`,`auto_scheduler.LocalRunner`。 + +``` python +def run_tuning(): + print("Begin tuning...") + tuner = auto_scheduler.TaskScheduler(tasks, task_weights) + tune_option = auto_scheduler.TuningOptions( + num_measure_trials=200, # 将此更改为 20000 以达到最佳性能 + runner=auto_scheduler.LocalRunner(repeat=10, enable_cpu_cache_flush=True), + measure_callbacks=[auto_scheduler.RecordToFile(log_file)], + ) + + if use_sparse: + from tvm.topi.sparse.utils import sparse_sketch_rules + + search_policy = [ + auto_scheduler.SketchPolicy( + task, + program_cost_model=auto_scheduler.XGBModel(), + init_search_callbacks=sparse_sketch_rules(), + ) + for task in tasks + ] + + tuner.tune(tune_option, search_policy=search_policy) + else: + tuner.tune(tune_option) + +# 不在网页服务器中运行调优,因为它需要的时间太长。 +# 取消注释运行以下行。 +# run_tuning() +``` + +:::note +解释调优过程中打印的信息 + +在调优过程中,控制台上会打印很多用于调试的信息,最重要的信息是任务调度程序的输出,下表是输出示例。 + +``` bash +---------------------------------------------------------------------- +------------------------------ [ Task Scheduler ] +---------------------------------------------------------------------- +| ID | Latency (ms) | Speed (GFLOPS) | Trials | +------------------------------------------------- +| 0 | 0.010 | 0.40 | 64 | +| 1 | 0.087 | 47.19 | 64 | +| 2 | 0.008 | -0.00 | 64 | +| 3 | 0.177 | 582.07 | 64 | +| 4 | 0.268 | 862.37 | 256 | +| 5 | 0.166 | 621.13 | 128 | +| 6 | 0.170 | 605.10 | 128 | +| 7 | 0.128 | 403.20 | 64 | +| 8 | 0.189 | 545.71 | 64 | +| 9 | 0.231 | 1001.01 | 448 | +| 10 | 0.155 | 664.80 | 256 | +| 11 | 0.155 | 662.86 | 256 | +| 12 | 0.119 | 434.08 | 64 | +| 13 | 0.199 | 522.13 | 64 | +| 14 | 0.235 | 986.56 | 320 | +| 15 | 0.149 | 689.13 | 128 | +| 16 | 0.155 | 664.80 | 192 | +| 17 | 0.151 | 340.64 | 64 | +| 18 | 0.176 | 597.55 | 128 | +| 19 | 0.220 | 1054.37 | 192 | +| 20 | 0.150 | 686.01 | 128 | +| 21 | 0.159 | 650.88 | 128 | +| 22 | 0.073 | 358.19 | 64 | +| 23 | 0.031 | 70.63 | 64 | +| 24 | 0.251 | 947.73 | 128 | +| 25 | 0.157 | 652.47 | 128 | +| 26 | 0.215 | 954.84 | 128 | +| 27 | 0.237 | 868.92 | 128 | +| 28 | 0.266 | 774.06 | 128 | +------------------------------------------------- +Estimated total latency: 10.016 ms Trials: 3992 Used time : 1131 s Next ID: 15 +``` + +此表列出了所有任务的延迟和(预估)速度,还列出了所有任务的测试分配。最后一行打印了这些任务的总加权延迟,可以粗略估计网络的端到端执行时间。最后一行还打印了测试的总数、自动调优所花费的总时间以及下一个要调优的任务的 ID。 + +还有一些「tvm::Error」错误,因为 auto-scheduler 会尝试一些无效的调度。若调优继续运行,则可以忽略这些错误,因为这些错误与主进程隔离。 +::: + +:::note +提前终止调优 + +可以通过强制终止此进程来提前终止调优,只要在日志文件中为每个任务获得至少一个有效的调度,就能够进行编译(下面的部分)。 +::: + +## 编译及评估 + +自动调优后,可以用找到的最佳调度来编译网络。在自动调优期间,所有测试记录都被转储到日志文件中,可以读取日志文件加载最佳调度。 + +``` python +# 用历史最佳编译 +print("Compile...") +with auto_scheduler.ApplyHistoryBest(log_file): + with tvm.transform.PassContext(opt_level=3, config={"relay.backend.use_auto_scheduler": True}): + lib = relay.build(mod, target=target, params=params) + +# 创建图执行器 +dev = tvm.device(str(target), 0) +module = graph_executor.GraphModule(lib["default"](dev)) +data_tvm = tvm.nd.array((np.random.uniform(size=input_shape)).astype(dtype)) +module.set_input("data", data_tvm) + +# 评估 +print("Evaluate inference time cost...") +print(module.benchmark(dev, repeat=3, min_repeat_ms=500)) +``` + +输出结果: + +``` bash +Compile... +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +Evaluate inference time cost... +Execution time summary: + mean (ms) median (ms) max (ms) min (ms) std (ms) + 762.1013 761.7971 762.8241 761.6828 0.5132 +``` + +## 其他技巧 + +1. 在调优过程中,auto-scheduler 需要编译许多程序并从中提取特征。这部分会占用大量 CPU 资源,所以推荐使用多核的高性能 CPU,加快搜索速度。 +2. 可以使用 `python3 -m tvm.auto_scheduler.measure_record --mode distill -i log.json` 提取大日志文件并仅保存最有用的记录。 +3. 可以从以前的日志文件恢复搜索,只需要在函数 `run_tuning` 中创建任务调度程序时添加一个新参数 `load_log_file`。比如,`tuner = auto_scheduler.TaskScheduler(tasks, task_weights, load_log_file=log_file)` +4. 若有多个 target CPU,则可以将所有这些 CPU 用于并行化测试。查看这 [部分](https://tvm.apache.org/docs/how_to/tune_with_autotvm/tune_relay_cuda.html#tutorials-autotvm-scale-up-rpc-tracker) 了解如何使用 RPC 跟踪器和 RPC 服务器。要在 auto-scheduler 中使用 RPC 跟踪器,请将 `TuningOptions` 中的 runner 替换为 `auto_scheduler.RPCRunner`。 + +**脚本总运行时长:**( 1 分 22.035 秒) + +[下载 Python 源代码:tune_network_x86.py](https://tvm.apache.org/docs/_downloads/e416b94ca1090b0897c0f6e0df95b911/tune_network_x86.py) + +[下载 Jupyter Notebook:tune_network_x86.ipynb](https://tvm.apache.org/docs/_downloads/ad2a7f55d615d188ad664d56696815a6/tune_network_x86.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/autoscheduler/03-autoschedule_nvidia.md b/versioned_docs/version-0.12.0/how_to/autoscheduler/03-autoschedule_nvidia.md new file mode 100644 index 00000000..7404e64a --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/autoscheduler/03-autoschedule_nvidia.md @@ -0,0 +1,528 @@ +--- +title: 为 NVIDIA GPU 自动调度神经网络 +--- + +# 为 NVIDIA GPU 自动调度神经网络 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/tune_with_autoscheduler/tune_network_cuda.html#sphx-glr-download-how-to-tune-with-autoscheduler-tune-network-cuda-py) 下载完整的示例代码 +::: + +**作者**:[Lianmin Zheng](https://github.com/merrymercy) + +针对特定设备和工作负载的自动调优对于获得最佳性能至关重要。本文介绍如何使用 auto-scheduler 为 NVIDIA GPU 调优整个神经网络。 + +为自动调优神经网络,需要将网络划分为小的子图并独立调优。每个子图被视为一个搜索任务,任务调度器对时间进行切片并动态地为这些任务分配时间资源,并预测每个任务对端到端执行时间的影响,优先考虑最能减少执行时间的任务。 + +对于每个子图,使用 `tvm/python/topi` 中的计算声明来获取张量表达式形式的计算 DAG。然后用 auto-scheduler 来构建这个 DAG 的搜索空间,并搜索合适的调度(低级优化)。 + +与基于 template 的 [AutoTVM](/docs/how_to/autotune)(依赖手动 template 来定义搜索空间的) 不同,auto-scheduler 无需任何调度 template。换言之,auto-scheduler 只使用 `tvm/python/topi` 中的计算声明,不使用现有的调度 template。 + +注意,本教程无法在 Windows 或最新版本的 macOS 上运行。如需运行,请将本教程的主体放在 `if __name__ == "__main__":` 代码块中。 + +``` python +import numpy as np + +import tvm +from tvm import relay, auto_scheduler +import tvm.relay.testing +from tvm.contrib import graph_executor +``` + +## 定义网络 + +首先,要用 Relay 前端 API 定义网络。可以从 `tvm.relay.testing` 加载一些预定义的网络。也可以从 MXNet、ONNX、PyTorch 和 TensorFlow 加载模型(参见 [前端教程](/docs/how_to/compile))。 + +对于卷积神经网络,尽管 auto-scheduler 可以在任何布局下正常运行,但通过 NHWC 布局实现的性能最佳。auto-scheduler 对 NHWC 布局进行了很多优化,因此推荐将模型转换为 NHWC 布局,从而得以使用 auto-scheduler。可用 [ConvertLayout](https://tvm.apache.org/docs/arch/convert_layout.html#convert-layout-usage) pass 在 TVM 中进行布局转换。 + +``` python +def get_network(name, batch_size, layout="NHWC", dtype="float32"): + """Get the symbol definition and random weight of a network""" + + # auto-scheduler 更适合 NHWC 布局 + if layout == "NHWC": + image_shape = (224, 224, 3) + elif layout == "NCHW": + image_shape = (3, 224, 224) + else: + raise ValueError("Invalid layout: " + layout) + + input_shape = (batch_size,) + image_shape + output_shape = (batch_size, 1000) + + if name.startswith("resnet-"): + n_layer = int(name.split("-")[1]) + mod, params = relay.testing.resnet.get_workload( + num_layers=n_layer, + batch_size=batch_size, + layout=layout, + dtype=dtype, + image_shape=image_shape, + ) + elif name.startswith("resnet3d-"): + n_layer = int(name.split("-")[1]) + mod, params = relay.testing.resnet.get_workload( + num_layers=n_layer, + batch_size=batch_size, + layout=layout, + dtype=dtype, + image_shape=image_shape, + ) + elif name == "mobilenet": + mod, params = relay.testing.mobilenet.get_workload( + batch_size=batch_size, layout=layout, dtype=dtype, image_shape=image_shape + ) + elif name == "squeezenet_v1.1": + assert layout == "NCHW", "squeezenet_v1.1 only supports NCHW layout" + mod, params = relay.testing.squeezenet.get_workload( + version="1.1", + batch_size=batch_size, + dtype=dtype, + image_shape=image_shape, + ) + elif name == "inception_v3": + input_shape = (batch_size, 3, 299, 299) if layout == "NCHW" else (batch_size, 299, 299, 3) + mod, params = relay.testing.inception_v3.get_workload(batch_size=batch_size, dtype=dtype) + elif name == "mxnet": + # MXNet 模型的示例 + from mxnet.gluon.model_zoo.vision import get_model + + assert layout == "NCHW" + + block = get_model("resnet18_v1", pretrained=True) + mod, params = relay.frontend.from_mxnet(block, shape={"data": input_shape}, dtype=dtype) + net = mod["main"] + net = relay.Function( + net.params, relay.nn.softmax(net.body), None, net.type_params, net.attrs + ) + mod = tvm.IRModule.from_expr(net) + + return mod, params, input_shape, output_shape + +# 定义神经网络和编译目标 +network = "resnet-18" +batch_size = 1 +layout = "NHWC" +target = tvm.target.Target("cuda") +dtype = "float32" +log_file = "%s-%s-B%d-%s.json" % (network, layout, batch_size, target.kind.name) +``` + +## 提取搜索任务 + +接下来,从网络中提取搜索任务及其权重。任务的权重是任务的子图在整个网络中出现的次数。通过使用权重,可以将网络的端到端延迟近似为 `sum(latency[t] * weight[t])`,其中 `latency[t]` 是任务的延迟,而 `weight[t]` 是任务的权重,任务调度器仅针对该目标进行优化。 + +``` python +# 从网络中提取任务 +print("Extract tasks...") +mod, params, input_shape, output_shape = get_network(network, batch_size, layout, dtype=dtype) +tasks, task_weights = auto_scheduler.extract_tasks(mod["main"], params, target) + +for idx, task in enumerate(tasks): + print("========== Task %d (workload key: %s) ==========" % (idx, task.workload_key)) + print(task.compute_dag) +``` + +输出结果: + +``` bash +Extract tasks... +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +========== Task 0 (workload key: ["8654f16aeddf785bad9f028164b3a48d", [1, 56, 56, 64], [1, 1, 64, 64], [1, 56, 56, 64]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 64] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 64, 64] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) + +========== Task 1 (workload key: ["c4500b4e2fd04e695c32d2f31bbdc14a", [1, 28, 28, 128], [4, 4, 128, 128], [1, 28, 28, 128], [1, 1, 1, 128], [1, 28, 28, 128]]) ========== +placeholder = PLACEHOLDER [1, 28, 28, 128] +data_pad(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 29)) && (i2 >= 1)) && (i2 < 29)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +input_tile(eps, nu, p, ci) = data_pad[floordiv(p, 196), ((floormod(floordiv(p, 14), 14)*2) + eps), ((floormod(p, 14)*2) + nu), ci] +B(i, j) = select(((floormod(i, 4) == 3) && (floormod(j, 4) == 3)), 1f, select(((floormod(i, 4) == 3) && (floormod(j, 4) == 2)), ..(OMITTED).. ormod(i, 4) == 0) && (floormod(j, 4) == 1)), 0f, select(((floormod(i, 4) == 0) && (floormod(j, 4) == 0)), 1f, 0f)))))))))))))))) +data_pack(eps, nu, p, ci) += ((input_tile[r_a, r_b, p, ci]*B[r_a, eps])*B[r_b, nu]) +placeholder = PLACEHOLDER [4, 4, 128, 128] +bgemm(eps, nu, p, co) += (data_pack[eps, nu, p, ci]*placeholder[eps, nu, co, ci]) +A(i, j) = select(((floormod(i, 4) == 3) && (floormod(j, 2) == 1)), 1f, select(((floormod(i, 4) == 3) && (floormod(j, 2) == 0)), ..(OMITTED).. ct(((floormod(i, 4) == 0) && (floormod(j, 2) == 1)), 0f, select(((floormod(i, 4) == 0) && (floormod(j, 2) == 0)), 1f, 0f)))))))) +inverse(vh, vw, p, co) += ((bgemm[r_a, r_b, p, co]*A[r_a, vh])*A[r_b, vw]) +conv2d_winograd(n, h, w, co) = inverse[floormod(h, 2), floormod(w, 2), ((((n*14)*14) + (floordiv(h, 2)*14)) + floordiv(w, 2)), co] +placeholder = PLACEHOLDER [1, 28, 28, 128] +T_add(ax0, ax1, ax2, ax3) = (conv2d_winograd[ax0, ax1, ax2, ax3] + placeholder[ax0, ax1, ax2, ax3]) +placeholder = PLACEHOLDER [1, 1, 1, 128] +T_add(ax0, ax1, ax2, ax3) = (T_add[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 2 (workload key: ["06f578e6519a86e85028eecf4de64b25", [1, 56, 56, 64], [1, 1, 64, 128], [1, 28, 28, 128]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 64] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 64, 128] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, ((yy*2) + ry), ((xx*2) + rx), rc]*placeholder[ry, rx, rc, ff]) + +========== Task 3 (workload key: ["b8b52b9be9df6102466a22a014c44c1f", [1, 14, 14, 256], [4, 4, 256, 256], [1, 1, 1, 256], [1, 14, 14, 256]]) ========== +placeholder = PLACEHOLDER [1, 14, 14, 256] +data_pad(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 15)) && (i2 >= 1)) && (i2 < 15)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +input_tile(eps, nu, p, ci) = data_pad[floordiv(p, 49), ((floormod(floordiv(p, 7), 7)*2) + eps), ((floormod(p, 7)*2) + nu), ci] +B(i, j) = select(((floormod(i, 4) == 3) && (floormod(j, 4) == 3)), 1f, select(((floormod(i, 4) == 3) && (floormod(j, 4) == 2)), ..(OMITTED).. ormod(i, 4) == 0) && (floormod(j, 4) == 1)), 0f, select(((floormod(i, 4) == 0) && (floormod(j, 4) == 0)), 1f, 0f)))))))))))))))) +data_pack(eps, nu, p, ci) += ((input_tile[r_a, r_b, p, ci]*B[r_a, eps])*B[r_b, nu]) +placeholder = PLACEHOLDER [4, 4, 256, 256] +bgemm(eps, nu, p, co) += (data_pack[eps, nu, p, ci]*placeholder[eps, nu, co, ci]) +A(i, j) = select(((floormod(i, 4) == 3) && (floormod(j, 2) == 1)), 1f, select(((floormod(i, 4) == 3) && (floormod(j, 2) == 0)), ..(OMITTED).. ct(((floormod(i, 4) == 0) && (floormod(j, 2) == 1)), 0f, select(((floormod(i, 4) == 0) && (floormod(j, 2) == 0)), 1f, 0f)))))))) +inverse(vh, vw, p, co) += ((bgemm[r_a, r_b, p, co]*A[r_a, vh])*A[r_b, vw]) +conv2d_winograd(n, h, w, co) = inverse[floormod(h, 2), floormod(w, 2), ((((n*7)*7) + (floordiv(h, 2)*7)) + floordiv(w, 2)), co] +placeholder = PLACEHOLDER [1, 1, 1, 256] +T_add(ax0, ax1, ax2, ax3) = (conv2d_winograd[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 4 (workload key: ["e4cdf917b876dbdd64488c3818d9c141", [1, 28, 28, 128], [4, 4, 128, 128], [1, 1, 1, 128], [1, 28, 28, 128]]) ========== +placeholder = PLACEHOLDER [1, 28, 28, 128] +data_pad(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 29)) && (i2 >= 1)) && (i2 < 29)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +input_tile(eps, nu, p, ci) = data_pad[floordiv(p, 196), ((floormod(floordiv(p, 14), 14)*2) + eps), ((floormod(p, 14)*2) + nu), ci] +B(i, j) = select(((floormod(i, 4) == 3) && (floormod(j, 4) == 3)), 1f, select(((floormod(i, 4) == 3) && (floormod(j, 4) == 2)), ..(OMITTED).. ormod(i, 4) == 0) && (floormod(j, 4) == 1)), 0f, select(((floormod(i, 4) == 0) && (floormod(j, 4) == 0)), 1f, 0f)))))))))))))))) +data_pack(eps, nu, p, ci) += ((input_tile[r_a, r_b, p, ci]*B[r_a, eps])*B[r_b, nu]) +placeholder = PLACEHOLDER [4, 4, 128, 128] +bgemm(eps, nu, p, co) += (data_pack[eps, nu, p, ci]*placeholder[eps, nu, co, ci]) +A(i, j) = select(((floormod(i, 4) == 3) && (floormod(j, 2) == 1)), 1f, select(((floormod(i, 4) == 3) && (floormod(j, 2) == 0)), ..(OMITTED).. ct(((floormod(i, 4) == 0) && (floormod(j, 2) == 1)), 0f, select(((floormod(i, 4) == 0) && (floormod(j, 2) == 0)), 1f, 0f)))))))) +inverse(vh, vw, p, co) += ((bgemm[r_a, r_b, p, co]*A[r_a, vh])*A[r_b, vw]) +conv2d_winograd(n, h, w, co) = inverse[floormod(h, 2), floormod(w, 2), ((((n*14)*14) + (floordiv(h, 2)*14)) + floordiv(w, 2)), co] +placeholder = PLACEHOLDER [1, 1, 1, 128] +T_add(ax0, ax1, ax2, ax3) = (conv2d_winograd[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 5 (workload key: ["d730bcd28f0920f6b97245e2a11bd8d6", [1, 7, 7, 512], [4, 4, 512, 512], [1, 7, 7, 512], [1, 7, 7, 512]]) ========== +placeholder = PLACEHOLDER [1, 7, 7, 512] +data_pad(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 8)) && (i2 >= 1)) && (i2 < 8)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +input_tile(eps, nu, p, ci) = data_pad[floordiv(p, 16), ((floormod(floordiv(p, 4), 4)*2) + eps), ((floormod(p, 4)*2) + nu), ci] +B(i, j) = select(((floormod(i, 4) == 3) && (floormod(j, 4) == 3)), 1f, select(((floormod(i, 4) == 3) && (floormod(j, 4) == 2)), ..(OMITTED).. ormod(i, 4) == 0) && (floormod(j, 4) == 1)), 0f, select(((floormod(i, 4) == 0) && (floormod(j, 4) == 0)), 1f, 0f)))))))))))))))) +data_pack(eps, nu, p, ci) += ((input_tile[r_a, r_b, p, ci]*B[r_a, eps])*B[r_b, nu]) +placeholder = PLACEHOLDER [4, 4, 512, 512] +bgemm(eps, nu, p, co) += (data_pack[eps, nu, p, ci]*placeholder[eps, nu, co, ci]) +A(i, j) = select(((floormod(i, 4) == 3) && (floormod(j, 2) == 1)), 1f, select(((floormod(i, 4) == 3) && (floormod(j, 2) == 0)), ..(OMITTED).. ct(((floormod(i, 4) == 0) && (floormod(j, 2) == 1)), 0f, select(((floormod(i, 4) == 0) && (floormod(j, 2) == 0)), 1f, 0f)))))))) +inverse(vh, vw, p, co) += ((bgemm[r_a, r_b, p, co]*A[r_a, vh])*A[r_b, vw]) +conv2d_winograd(n, h, w, co) = inverse[floormod(h, 2), floormod(w, 2), ((((n*4)*4) + (floordiv(h, 2)*4)) + floordiv(w, 2)), co] +placeholder = PLACEHOLDER [1, 7, 7, 512] +T_add(ax0, ax1, ax2, ax3) = (conv2d_winograd[ax0, ax1, ax2, ax3] + placeholder[ax0, ax1, ax2, ax3]) + +========== Task 6 (workload key: ["b818b53148cd450f86569dfc3e04cb8a", [1, 56, 56, 64], [6, 6, 64, 64], [1, 1, 1, 64], [1, 56, 56, 64]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 64] +data_pad(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 57)) && (i2 >= 1)) && (i2 < 57)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +input_tile(eps, nu, p, ci) = data_pad[floordiv(p, 196), ((floormod(floordiv(p, 14), 14)*4) + eps), ((floormod(p, 14)*4) + nu), ci] +B(i, j) = select(((floormod(i, 6) == 5) && (floormod(j, 6) == 5)), 1f, select(((floormod(i, 6) == 5) && (floormod(j, 6) == 4)), ..(OMITTED).. (floormod(j, 6) == 1)), 0f, select(((floormod(i, 6) == 0) && (floormod(j, 6) == 0)), 1f, 0f)))))))))))))))))))))))))))))))))))) +data_pack(eps, nu, p, ci) += ((input_tile[r_a, r_b, p, ci]*B[r_a, eps])*B[r_b, nu]) +placeholder = PLACEHOLDER [6, 6, 64, 64] +bgemm(eps, nu, p, co) += (data_pack[eps, nu, p, ci]*placeholder[eps, nu, co, ci]) +A(i, j) = select(((floormod(i, 6) == 5) && (floormod(j, 4) == 3)), 1f, select(((floormod(i, 6) == 5) && (floormod(j, 4) == 2)), ..(OMITTED).. 6) == 0) && (floormod(j, 4) == 1)), 0f, select(((floormod(i, 6) == 0) && (floormod(j, 4) == 0)), 1f, 0f)))))))))))))))))))))))) +inverse(vh, vw, p, co) += ((bgemm[r_a, r_b, p, co]*A[r_a, vh])*A[r_b, vw]) +conv2d_winograd(n, h, w, co) = inverse[floormod(h, 4), floormod(w, 4), ((((n*14)*14) + (floordiv(h, 4)*14)) + floordiv(w, 4)), co] +placeholder = PLACEHOLDER [1, 1, 1, 64] +T_add(ax0, ax1, ax2, ax3) = (conv2d_winograd[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 7 (workload key: ["ad6cecbf5d85cb1cda3c2bb7af170211", [1, 7, 7, 512], [4, 4, 512, 512], [1, 7, 7, 512], [1, 1, 1, 512], [1, 1, 1, 512], [1, 7, 7, 512]]) ========== +placeholder = PLACEHOLDER [1, 7, 7, 512] +data_pad(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 8)) && (i2 >= 1)) && (i2 < 8)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +input_tile(eps, nu, p, ci) = data_pad[floordiv(p, 16), ((floormod(floordiv(p, 4), 4)*2) + eps), ((floormod(p, 4)*2) + nu), ci] +B(i, j) = select(((floormod(i, 4) == 3) && (floormod(j, 4) == 3)), 1f, select(((floormod(i, 4) == 3) && (floormod(j, 4) == 2)), ..(OMITTED).. ormod(i, 4) == 0) && (floormod(j, 4) == 1)), 0f, select(((floormod(i, 4) == 0) && (floormod(j, 4) == 0)), 1f, 0f)))))))))))))))) +data_pack(eps, nu, p, ci) += ((input_tile[r_a, r_b, p, ci]*B[r_a, eps])*B[r_b, nu]) +placeholder = PLACEHOLDER [4, 4, 512, 512] +bgemm(eps, nu, p, co) += (data_pack[eps, nu, p, ci]*placeholder[eps, nu, co, ci]) +A(i, j) = select(((floormod(i, 4) == 3) && (floormod(j, 2) == 1)), 1f, select(((floormod(i, 4) == 3) && (floormod(j, 2) == 0)), ..(OMITTED).. ct(((floormod(i, 4) == 0) && (floormod(j, 2) == 1)), 0f, select(((floormod(i, 4) == 0) && (floormod(j, 2) == 0)), 1f, 0f)))))))) +inverse(vh, vw, p, co) += ((bgemm[r_a, r_b, p, co]*A[r_a, vh])*A[r_b, vw]) +conv2d_winograd(n, h, w, co) = inverse[floormod(h, 2), floormod(w, 2), ((((n*4)*4) + (floordiv(h, 2)*4)) + floordiv(w, 2)), co] +placeholder = PLACEHOLDER [1, 7, 7, 512] +T_add(ax0, ax1, ax2, ax3) = (conv2d_winograd[ax0, ax1, ax2, ax3] + placeholder[ax0, ax1, ax2, ax3]) +placeholder = PLACEHOLDER [1, 1, 1, 512] +T_multiply(ax0, ax1, ax2, ax3) = (T_add[ax0, ax1, ax2, ax3]*placeholder[ax0, 0, 0, ax3]) +placeholder = PLACEHOLDER [1, 1, 1, 512] +T_add(ax0, ax1, ax2, ax3) = (T_multiply[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 8 (workload key: ["f3b6c10fcc6ce01ff01add933e4d21e9", [1, 14, 14, 256], [4, 4, 256, 256], [1, 14, 14, 256], [1, 1, 1, 256], [1, 14, 14, 256]]) ========== +placeholder = PLACEHOLDER [1, 14, 14, 256] +data_pad(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 15)) && (i2 >= 1)) && (i2 < 15)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +input_tile(eps, nu, p, ci) = data_pad[floordiv(p, 49), ((floormod(floordiv(p, 7), 7)*2) + eps), ((floormod(p, 7)*2) + nu), ci] +B(i, j) = select(((floormod(i, 4) == 3) && (floormod(j, 4) == 3)), 1f, select(((floormod(i, 4) == 3) && (floormod(j, 4) == 2)), ..(OMITTED).. ormod(i, 4) == 0) && (floormod(j, 4) == 1)), 0f, select(((floormod(i, 4) == 0) && (floormod(j, 4) == 0)), 1f, 0f)))))))))))))))) +data_pack(eps, nu, p, ci) += ((input_tile[r_a, r_b, p, ci]*B[r_a, eps])*B[r_b, nu]) +placeholder = PLACEHOLDER [4, 4, 256, 256] +bgemm(eps, nu, p, co) += (data_pack[eps, nu, p, ci]*placeholder[eps, nu, co, ci]) +A(i, j) = select(((floormod(i, 4) == 3) && (floormod(j, 2) == 1)), 1f, select(((floormod(i, 4) == 3) && (floormod(j, 2) == 0)), ..(OMITTED).. ct(((floormod(i, 4) == 0) && (floormod(j, 2) == 1)), 0f, select(((floormod(i, 4) == 0) && (floormod(j, 2) == 0)), 1f, 0f)))))))) +inverse(vh, vw, p, co) += ((bgemm[r_a, r_b, p, co]*A[r_a, vh])*A[r_b, vw]) +conv2d_winograd(n, h, w, co) = inverse[floormod(h, 2), floormod(w, 2), ((((n*7)*7) + (floordiv(h, 2)*7)) + floordiv(w, 2)), co] +placeholder = PLACEHOLDER [1, 14, 14, 256] +T_add(ax0, ax1, ax2, ax3) = (conv2d_winograd[ax0, ax1, ax2, ax3] + placeholder[ax0, ax1, ax2, ax3]) +placeholder = PLACEHOLDER [1, 1, 1, 256] +T_add(ax0, ax1, ax2, ax3) = (T_add[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 9 (workload key: ["d7b65649a4dd54becea0a52aabbc5af5", [1, 1000], [1, 1000]]) ========== +placeholder = PLACEHOLDER [1, 1000] +T_softmax_maxelem(i0) max= placeholder[i0, k] +T_softmax_exp(i0, i1) = tir.exp((placeholder[i0, i1] - T_softmax_maxelem[i0])) +T_softmax_expsum(i0) += T_softmax_exp[i0, k] +T_softmax_norm(i0, i1) = (T_softmax_exp[i0, i1]/T_softmax_expsum[i0]) + +========== Task 10 (workload key: ["69115f188984ae34ede37c3b8ca40b43", [1, 7, 7, 512], [1, 1, 1, 512]]) ========== +placeholder = PLACEHOLDER [1, 7, 7, 512] +tensor(ax0, ax1, ax2, ax3) += placeholder[ax0, ((ax1*7) + rv0), ((ax2*7) + rv1), ax3] +tensor(ax0, ax1, ax2, ax3) = (tensor[ax0, ax1, ax2, ax3]/(float32((select((bool)1, ((ax1 + 1)*7), (((ax1 + 1)*7) + 1)) - (ax1*7)))*float32((select((bool)1, ((ax2 + 1)*7), (((ax2 + 1)*7) + 1)) - (ax2*7))))) + +========== Task 11 (workload key: ["3a69f9fbc63760d99e36b4c17b3bfc57", [1, 7, 7, 512], [4, 4, 512, 512], [1, 1, 1, 512], [1, 7, 7, 512]]) ========== +placeholder = PLACEHOLDER [1, 7, 7, 512] +data_pad(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 8)) && (i2 >= 1)) && (i2 < 8)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +input_tile(eps, nu, p, ci) = data_pad[floordiv(p, 16), ((floormod(floordiv(p, 4), 4)*2) + eps), ((floormod(p, 4)*2) + nu), ci] +B(i, j) = select(((floormod(i, 4) == 3) && (floormod(j, 4) == 3)), 1f, select(((floormod(i, 4) == 3) && (floormod(j, 4) == 2)), ..(OMITTED).. ormod(i, 4) == 0) && (floormod(j, 4) == 1)), 0f, select(((floormod(i, 4) == 0) && (floormod(j, 4) == 0)), 1f, 0f)))))))))))))))) +data_pack(eps, nu, p, ci) += ((input_tile[r_a, r_b, p, ci]*B[r_a, eps])*B[r_b, nu]) +placeholder = PLACEHOLDER [4, 4, 512, 512] +bgemm(eps, nu, p, co) += (data_pack[eps, nu, p, ci]*placeholder[eps, nu, co, ci]) +A(i, j) = select(((floormod(i, 4) == 3) && (floormod(j, 2) == 1)), 1f, select(((floormod(i, 4) == 3) && (floormod(j, 2) == 0)), ..(OMITTED).. ct(((floormod(i, 4) == 0) && (floormod(j, 2) == 1)), 0f, select(((floormod(i, 4) == 0) && (floormod(j, 2) == 0)), 1f, 0f)))))))) +inverse(vh, vw, p, co) += ((bgemm[r_a, r_b, p, co]*A[r_a, vh])*A[r_b, vw]) +conv2d_winograd(n, h, w, co) = inverse[floormod(h, 2), floormod(w, 2), ((((n*4)*4) + (floordiv(h, 2)*4)) + floordiv(w, 2)), co] +placeholder = PLACEHOLDER [1, 1, 1, 512] +T_add(ax0, ax1, ax2, ax3) = (conv2d_winograd[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 12 (workload key: ["06f578e6519a86e85028eecf4de64b25", [1, 28, 28, 128], [1, 1, 128, 256], [1, 14, 14, 256]]) ========== +placeholder = PLACEHOLDER [1, 28, 28, 128] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 128, 256] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, ((yy*2) + ry), ((xx*2) + rx), rc]*placeholder[ry, rx, rc, ff]) + +========== Task 13 (workload key: ["96daaa9daa1b41bc383b7c05ce8b58de", [1, 14, 14, 256], [3, 3, 256, 512], [1, 1, 1, 512], [1, 7, 7, 512]]) ========== +placeholder = PLACEHOLDER [1, 14, 14, 256] +pad_temp(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 15)) && (i2 >= 1)) && (i2 < 15)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 256, 512] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, ((yy*2) + ry), ((xx*2) + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 512] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 14 (workload key: ["dac19035dd5fe9424ee8617421b9c817", [1, 28, 28, 128], [4, 4, 128, 128], [1, 28, 28, 128], [1, 28, 28, 128]]) ========== +placeholder = PLACEHOLDER [1, 28, 28, 128] +data_pad(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 29)) && (i2 >= 1)) && (i2 < 29)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +input_tile(eps, nu, p, ci) = data_pad[floordiv(p, 196), ((floormod(floordiv(p, 14), 14)*2) + eps), ((floormod(p, 14)*2) + nu), ci] +B(i, j) = select(((floormod(i, 4) == 3) && (floormod(j, 4) == 3)), 1f, select(((floormod(i, 4) == 3) && (floormod(j, 4) == 2)), ..(OMITTED).. ormod(i, 4) == 0) && (floormod(j, 4) == 1)), 0f, select(((floormod(i, 4) == 0) && (floormod(j, 4) == 0)), 1f, 0f)))))))))))))))) +data_pack(eps, nu, p, ci) += ((input_tile[r_a, r_b, p, ci]*B[r_a, eps])*B[r_b, nu]) +placeholder = PLACEHOLDER [4, 4, 128, 128] +bgemm(eps, nu, p, co) += (data_pack[eps, nu, p, ci]*placeholder[eps, nu, co, ci]) +A(i, j) = select(((floormod(i, 4) == 3) && (floormod(j, 2) == 1)), 1f, select(((floormod(i, 4) == 3) && (floormod(j, 2) == 0)), ..(OMITTED).. ct(((floormod(i, 4) == 0) && (floormod(j, 2) == 1)), 0f, select(((floormod(i, 4) == 0) && (floormod(j, 2) == 0)), 1f, 0f)))))))) +inverse(vh, vw, p, co) += ((bgemm[r_a, r_b, p, co]*A[r_a, vh])*A[r_b, vw]) +conv2d_winograd(n, h, w, co) = inverse[floormod(h, 2), floormod(w, 2), ((((n*14)*14) + (floordiv(h, 2)*14)) + floordiv(w, 2)), co] +placeholder = PLACEHOLDER [1, 28, 28, 128] +T_add(ax0, ax1, ax2, ax3) = (conv2d_winograd[ax0, ax1, ax2, ax3] + placeholder[ax0, ax1, ax2, ax3]) + +========== Task 15 (workload key: ["96daaa9daa1b41bc383b7c05ce8b58de", [1, 28, 28, 128], [3, 3, 128, 256], [1, 1, 1, 256], [1, 14, 14, 256]]) ========== +placeholder = PLACEHOLDER [1, 28, 28, 128] +pad_temp(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 29)) && (i2 >= 1)) && (i2 < 29)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 128, 256] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, ((yy*2) + ry), ((xx*2) + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 256] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 16 (workload key: ["1e3c4211ffd2f2db91078ae4d04b779d", [1, 56, 56, 64], [6, 6, 64, 64], [1, 56, 56, 64], [1, 1, 1, 64], [1, 56, 56, 64]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 64] +data_pad(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 57)) && (i2 >= 1)) && (i2 < 57)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +input_tile(eps, nu, p, ci) = data_pad[floordiv(p, 196), ((floormod(floordiv(p, 14), 14)*4) + eps), ((floormod(p, 14)*4) + nu), ci] +B(i, j) = select(((floormod(i, 6) == 5) && (floormod(j, 6) == 5)), 1f, select(((floormod(i, 6) == 5) && (floormod(j, 6) == 4)), ..(OMITTED).. (floormod(j, 6) == 1)), 0f, select(((floormod(i, 6) == 0) && (floormod(j, 6) == 0)), 1f, 0f)))))))))))))))))))))))))))))))))))) +data_pack(eps, nu, p, ci) += ((input_tile[r_a, r_b, p, ci]*B[r_a, eps])*B[r_b, nu]) +placeholder = PLACEHOLDER [6, 6, 64, 64] +bgemm(eps, nu, p, co) += (data_pack[eps, nu, p, ci]*placeholder[eps, nu, co, ci]) +A(i, j) = select(((floormod(i, 6) == 5) && (floormod(j, 4) == 3)), 1f, select(((floormod(i, 6) == 5) && (floormod(j, 4) == 2)), ..(OMITTED).. 6) == 0) && (floormod(j, 4) == 1)), 0f, select(((floormod(i, 6) == 0) && (floormod(j, 4) == 0)), 1f, 0f)))))))))))))))))))))))) +inverse(vh, vw, p, co) += ((bgemm[r_a, r_b, p, co]*A[r_a, vh])*A[r_b, vw]) +conv2d_winograd(n, h, w, co) = inverse[floormod(h, 4), floormod(w, 4), ((((n*14)*14) + (floordiv(h, 4)*14)) + floordiv(w, 4)), co] +placeholder = PLACEHOLDER [1, 56, 56, 64] +T_add(ax0, ax1, ax2, ax3) = (conv2d_winograd[ax0, ax1, ax2, ax3] + placeholder[ax0, ax1, ax2, ax3]) +placeholder = PLACEHOLDER [1, 1, 1, 64] +T_add(ax0, ax1, ax2, ax3) = (T_add[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 17 (workload key: ["96daaa9daa1b41bc383b7c05ce8b58de", [1, 224, 224, 3], [7, 7, 3, 64], [1, 1, 1, 64], [1, 112, 112, 64]]) ========== +placeholder = PLACEHOLDER [1, 224, 224, 3] +pad_temp(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 3) && (i1 < 227)) && (i2 >= 3)) && (i2 < 227)), placeholder[i0, (i1 - 3), (i2 - 3), i3], 0f) +placeholder = PLACEHOLDER [7, 7, 3, 64] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, ((yy*2) + ry), ((xx*2) + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 64] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 18 (workload key: ["3ea73fb9b0364374730d09e068821f95", [1, 56, 56, 64], [6, 6, 64, 64], [1, 56, 56, 64], [1, 56, 56, 64]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 64] +data_pad(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 57)) && (i2 >= 1)) && (i2 < 57)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +input_tile(eps, nu, p, ci) = data_pad[floordiv(p, 196), ((floormod(floordiv(p, 14), 14)*4) + eps), ((floormod(p, 14)*4) + nu), ci] +B(i, j) = select(((floormod(i, 6) == 5) && (floormod(j, 6) == 5)), 1f, select(((floormod(i, 6) == 5) && (floormod(j, 6) == 4)), ..(OMITTED).. (floormod(j, 6) == 1)), 0f, select(((floormod(i, 6) == 0) && (floormod(j, 6) == 0)), 1f, 0f)))))))))))))))))))))))))))))))))))) +data_pack(eps, nu, p, ci) += ((input_tile[r_a, r_b, p, ci]*B[r_a, eps])*B[r_b, nu]) +placeholder = PLACEHOLDER [6, 6, 64, 64] +bgemm(eps, nu, p, co) += (data_pack[eps, nu, p, ci]*placeholder[eps, nu, co, ci]) +A(i, j) = select(((floormod(i, 6) == 5) && (floormod(j, 4) == 3)), 1f, select(((floormod(i, 6) == 5) && (floormod(j, 4) == 2)), ..(OMITTED).. 6) == 0) && (floormod(j, 4) == 1)), 0f, select(((floormod(i, 6) == 0) && (floormod(j, 4) == 0)), 1f, 0f)))))))))))))))))))))))) +inverse(vh, vw, p, co) += ((bgemm[r_a, r_b, p, co]*A[r_a, vh])*A[r_b, vw]) +conv2d_winograd(n, h, w, co) = inverse[floormod(h, 4), floormod(w, 4), ((((n*14)*14) + (floordiv(h, 4)*14)) + floordiv(w, 4)), co] +placeholder = PLACEHOLDER [1, 56, 56, 64] +T_add(ax0, ax1, ax2, ax3) = (conv2d_winograd[ax0, ax1, ax2, ax3] + placeholder[ax0, ax1, ax2, ax3]) + +========== Task 19 (workload key: ["d374e472bd9d8164892b9e28a0a8cb59", [1, 14, 14, 256], [4, 4, 256, 256], [1, 14, 14, 256], [1, 14, 14, 256]]) ========== +placeholder = PLACEHOLDER [1, 14, 14, 256] +data_pad(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 15)) && (i2 >= 1)) && (i2 < 15)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +input_tile(eps, nu, p, ci) = data_pad[floordiv(p, 49), ((floormod(floordiv(p, 7), 7)*2) + eps), ((floormod(p, 7)*2) + nu), ci] +B(i, j) = select(((floormod(i, 4) == 3) && (floormod(j, 4) == 3)), 1f, select(((floormod(i, 4) == 3) && (floormod(j, 4) == 2)), ..(OMITTED).. ormod(i, 4) == 0) && (floormod(j, 4) == 1)), 0f, select(((floormod(i, 4) == 0) && (floormod(j, 4) == 0)), 1f, 0f)))))))))))))))) +data_pack(eps, nu, p, ci) += ((input_tile[r_a, r_b, p, ci]*B[r_a, eps])*B[r_b, nu]) +placeholder = PLACEHOLDER [4, 4, 256, 256] +bgemm(eps, nu, p, co) += (data_pack[eps, nu, p, ci]*placeholder[eps, nu, co, ci]) +A(i, j) = select(((floormod(i, 4) == 3) && (floormod(j, 2) == 1)), 1f, select(((floormod(i, 4) == 3) && (floormod(j, 2) == 0)), ..(OMITTED).. ct(((floormod(i, 4) == 0) && (floormod(j, 2) == 1)), 0f, select(((floormod(i, 4) == 0) && (floormod(j, 2) == 0)), 1f, 0f)))))))) +inverse(vh, vw, p, co) += ((bgemm[r_a, r_b, p, co]*A[r_a, vh])*A[r_b, vw]) +conv2d_winograd(n, h, w, co) = inverse[floormod(h, 2), floormod(w, 2), ((((n*7)*7) + (floordiv(h, 2)*7)) + floordiv(w, 2)), co] +placeholder = PLACEHOLDER [1, 14, 14, 256] +T_add(ax0, ax1, ax2, ax3) = (conv2d_winograd[ax0, ax1, ax2, ax3] + placeholder[ax0, ax1, ax2, ax3]) + +========== Task 20 (workload key: ["64b98c71af70a904fdbb81d7d4188d84", [1, 112, 112, 64], [1, 1, 1, 64], [1, 56, 56, 64]]) ========== +placeholder = PLACEHOLDER [1, 112, 112, 64] +pad_temp(ax0, ax1, ax2, ax3) = tir.if_then_else(((((ax1 >= 1) && (ax1 < 113)) && (ax2 >= 1)) && (ax2 < 113)), placeholder[ax0, (ax1 - 1), (ax2 - 1), ax3], -3.40282e+38f) +tensor(ax0, ax1, ax2, ax3) max= pad_temp[ax0, ((ax1*2) + rv0), ((ax2*2) + rv1), ax3] +placeholder = PLACEHOLDER [1, 1, 1, 64] +T_add(ax0, ax1, ax2, ax3) = (tensor[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 21 (workload key: ["06f578e6519a86e85028eecf4de64b25", [1, 14, 14, 256], [1, 1, 256, 512], [1, 7, 7, 512]]) ========== +placeholder = PLACEHOLDER [1, 14, 14, 256] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 256, 512] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, ((yy*2) + ry), ((xx*2) + rx), rc]*placeholder[ry, rx, rc, ff]) + +========== Task 22 (workload key: ["7d44c6e3c81cd80f61ff2265b2bae89a", [1, 512], [1000, 512], [1, 1000], [1, 1000]]) ========== +placeholder = PLACEHOLDER [1, 512] +placeholder = PLACEHOLDER [1000, 512] +T_matmul_NT(i, j) += (placeholder[i, k]*placeholder[j, k]) +placeholder = PLACEHOLDER [1, 1000] +T_add(ax0, ax1) = (T_matmul_NT[ax0, ax1] + placeholder[ax0, ax1]) + +========== Task 23 (workload key: ["96daaa9daa1b41bc383b7c05ce8b58de", [1, 56, 56, 64], [3, 3, 64, 128], [1, 1, 1, 128], [1, 28, 28, 128]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 64] +pad_temp(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 57)) && (i2 >= 1)) && (i2 < 57)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 64, 128] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, ((yy*2) + ry), ((xx*2) + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 128] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) +``` + +## 开始调优 + +接下来为调优和启动搜索任务设置一些选项 + +* `measure_ctx` 启动不同的测试过程以提供隔离。在测试期间保护主进程免受 GPU 崩溃并避免其他 runtime 冲突。 +* `min_repeat_ms` 定义每次测试中一次“重复”的最短持续时间,可以预热 GPU 以获得准确测试结果,通常,推荐设置值 >= 300 ms。 +* `num_measure_trials` 是调优期间可以使用的测试次数(根据自己的时间预算调整这个参数),若要快速演示,可将其设置为较小的数字(例如 200)。推荐将其设置为 `900 * len(tasks)` 左右,以便使搜索收敛。比如 resnet-18 有 24 个任务,所以可以设置为 20000。 +* 此外,使用 `RecordToFile` 将测试记录转储到日志文件中,测试记录可用于历史最佳查询、恢复搜索以及进行后续分析。 +* 更多参数参见 `auto_scheduler.TuningOptions`,`auto_scheduler.LocalRunner`。 + +``` python +def run_tuning(): + print("Begin tuning...") + measure_ctx = auto_scheduler.LocalRPCMeasureContext(repeat=1, min_repeat_ms=300, timeout=10) + + tuner = auto_scheduler.TaskScheduler(tasks, task_weights) + tune_option = auto_scheduler.TuningOptions( + num_measure_trials=200, # 将此更改为 20000 以达到最佳性能 + runner=measure_ctx.runner, + measure_callbacks=[auto_scheduler.RecordToFile(log_file)], + ) + + tuner.tune(tune_option) + +# 不在网页服务器中运行调优,因为它需要的时间太长。 +# 取消注释运行下面行。 +# run_tuning() +``` + +:::note +解释调优过程中打印的信息 + +在调优过程中,控制台上会打印很多用于调试的信息,最重要的信息是任务调度程序的输出,下表是输出示例。 + +``` bash +---------------------------------------------------------------------- +------------------------------ [ Task Scheduler ] +---------------------------------------------------------------------- +| ID | Latency (ms) | Speed (GFLOPS) | Trials | +------------------------------------------------- +| 0 | 0.005 | 0.88 | 64 | +| 1 | 0.010 | 99.10 | 64 | +| 2 | 0.006 | 0.00 | 64 | +| 3 | 0.145 | 979.78 | 384 | +| 4 | 0.130 | 1097.02 | 384 | +| 5 | 0.143 | 992.69 | 384 | +| 6 | 0.076 | 1526.86 | 192 | +| 7 | 0.115 | 999.44 | 320 | +| 8 | 0.079 | 1449.39 | 320 | +| 9 | 0.122 | 938.73 | 384 | +| 10 | 0.063 | 1832.98 | 192 | +| 11 | 0.072 | 1763.62 | 256 | +| 12 | 0.062 | 2036.40 | 192 | +| 13 | 0.068 | 1874.44 | 192 | +| 14 | 0.049 | 2346.50 | 128 | +| 15 | 0.076 | 1694.31 | 256 | +| 16 | 0.067 | 1933.30 | 448 | +| 17 | 0.076 | 1680.90 | 256 | +| 18 | 0.022 | 98.43 | 64 | +| 19 | 0.076 | 3112.55 | 192 | +| 20 | 0.013 | 2026.44 | 64 | +| 21 | 0.011 | 1136.69 | 64 | +| 22 | 0.013 | 992.47 | 64 | +| 23 | 0.020 | 627.56 | 64 | +------------------------------------------------- +Estimated total latency: 1.587 ms Trials: 4992 Used time : 13296 s Next ID: 3 +``` + +此表列出了所有任务的延迟和(预估)速度,还列出了所有任务的测试分配。最后一行打印了这些任务的总加权延迟,可以粗略估计网络的端到端执行时间。最后一行还打印了测试试验的总数、自动调优所花费的总时间以及下一个要调优的任务的 ID。 + +还有一些「tvm::Error」错误,因为 auto-scheduler 会尝试一些无效的调度。若调优继续运行,则可以忽略这些错误,因为这些错误与主进程隔离。 +::: + +:::note +提前终止调优 + +可以通过强制终止此进程来提前终止调优,只要在日志文件中为每个任务获得至少一个有效的调度,就能够进行编译(下面的部分)。 +::: + +## 编译及评估 + +自动调优后,用找到的最佳调度来编译网络。在自动调优期间,所有测试记录都被转储到日志文件中,可以读取日志文件加载最佳调度。 + +``` python +# 用历史最佳编译 +print("Compile...") +with auto_scheduler.ApplyHistoryBest(log_file): + with tvm.transform.PassContext(opt_level=3, config={"relay.backend.use_auto_scheduler": True}): + lib = relay.build(mod, target=target, params=params) + +# 创建图执行器 +dev = tvm.device(str(target), 0) +module = graph_executor.GraphModule(lib["default"](dev)) +data_tvm = tvm.nd.array((np.random.uniform(size=input_shape)).astype(dtype)) +module.set_input("data", data_tvm) + +# 评估 +print("Evaluate inference time cost...") +print(module.benchmark(dev, repeat=3, min_repeat_ms=500)) +``` + +输出结果: + +``` bash +Compile... +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +Evaluate inference time cost... +Execution time summary: + mean (ms) median (ms) max (ms) min (ms) std (ms) + 10.0003 9.9944 10.0327 9.9738 0.0244 +``` + +## 其他技巧 + +1. 在调优过程中,auto-scheduler 需要编译许多程序,并从中提取特征。这部分会占用大量 CPU 资源,所以推荐使用多核的高性能 CPU,加快搜索速度。 +2. 可以用 `python3 -m tvm.auto_scheduler.measure_record --mode distill -i log.json` 提取大日志文件,并仅保存最有用的记录。 +3. 可以从以前的日志文件恢复搜索,只需要在函数 `run_tuning` 中创建任务调度程序时添加一个新参数 `load_log_file`。比如,`tuner = auto_scheduler.TaskScheduler(tasks, task_weights, load_log_file=log_file)` +4. 若有多个 target CPU,则可以将所有这些 CPU 用于并行化测试。查看这 [部分](https://tvm.apache.org/docs/how_to/tune_with_autotvm/tune_relay_cuda.html#tutorials-autotvm-scale-up-rpc-tracker) 了解如何使用 RPC 跟踪器和 RPC 服务器。要在 auto-scheduler 中使用 RPC 跟踪器,请将 `TuningOptions` 中的 runner 替换为 `auto_scheduler.RPCRunner`。 + +[下载 Python 源代码:tune_network_cuda.py](https://tvm.apache.org/docs/_downloads/eafe360d52540634c9eea0fa89e804bd/tune_network_cuda.py) + +[下载 Jupyter Notebook:tune_network_cuda.ipynb](https://tvm.apache.org/docs/_downloads/af264436d049e3cd84803b67b6620b63/tune_network_cuda.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/autoscheduler/04-autoschedule_arm.md b/versioned_docs/version-0.12.0/how_to/autoscheduler/04-autoschedule_arm.md new file mode 100644 index 00000000..5e5dccd0 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/autoscheduler/04-autoschedule_arm.md @@ -0,0 +1,547 @@ +--- +title: 为 ARM CPU 自动调度神经网络 +--- + +# 为 ARM CPU 自动调度神经网络 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/tune_with_autoscheduler/tune_network_arm.html#sphx-glr-download-how-to-tune-with-autoscheduler-tune-network-arm-py) 下载完整的示例代码 +::: + +**作者**:[Thierry Moreau](https://github.com/tmoreau89), [Lianmin Zheng](https://github.com/merrymercy), [Chengfan Jia](https://github.com/jcf94/) + +针对特定设备和工作负载的自动调优对于获得最佳性能至关重要。本文介绍如何通过 RPC 使用 auto-scheduler 为 ARM CPU 调优整个神经网络。 + +为了自动调优神经网络,将网络划分为小的子图并独立进行调优。每个子图被视为一个搜索任务。任务调度器对时间进行切片,并动态地为这些任务分配时间资源,预测每个任务对端到端执行时间的影响,并优先考虑最能减少执行时间的任务。 + +对于每个子图,使用 `tvm/python/topi` 中的计算声明来获取张量表达式形式的计算 DAG。然后使用 auto-scheduler 来构建这个 DAG 的搜索空间,并搜索合适的调度(底层优化)。 + +与基于 template 的 [AutoTVM](/docs/how_to/autotune)(依赖手动 template 来定义搜索空间的) 不同,auto-scheduler 不需要任何调度 template。换言之,auto-scheduler 只使用 `tvm/python/topi` 中的计算声明,不使用现有的调度 template。 + +注意,本教程无法在 Windows 或最新版本的 macOS 上运行。如需运行,请将本教程的主体放在 `if __name__ == "__main__":` 代码块中。 + +``` python +import numpy as np +import os + +import tvm +from tvm import relay, auto_scheduler +from tvm.relay import data_dep_optimization as ddo +import tvm.relay.testing +from tvm.contrib import graph_executor +from tvm.contrib.utils import tempdir +``` + +## 定义网络 + +首先,要用 Relay 前端 API 定义网络。可以从 `tvm.relay.testing` 加载一些预定义的网络。也可以从 MXNet、ONNX、PyTorch 和 TensorFlow 加载模型(参见 [前端教程](/docs/how_to/compile)))。 + +对于卷积神经网络,尽管 auto-scheduler 可以在任何布局下正常运行,但通过 NHWC 布局实现的性能最佳。auto-scheduler 对 NHWC 布局进行了很多优化,因此推荐将模型转换为 NHWC 布局,从而得以使用 auto-scheduler。可用 [ConvertLayout](https://tvm.apache.org/docs/arch/convert_layout.html#convert-layout-usage) pass 在 TVM 中进行布局转换。 + +``` python +def get_network(name, batch_size, layout="NHWC", dtype="float32", use_sparse=False): + """获取网络的符号定义和随机权重""" + + # auto-scheduler 更适合 NHWC 布局 + if layout == "NHWC": + image_shape = (224, 224, 3) + elif layout == "NCHW": + image_shape = (3, 224, 224) + else: + raise ValueError("Invalid layout: " + layout) + + input_shape = (batch_size,) + image_shape + output_shape = (batch_size, 1000) + + if name.startswith("resnet-"): + n_layer = int(name.split("-")[1]) + mod, params = relay.testing.resnet.get_workload( + num_layers=n_layer, + batch_size=batch_size, + layout=layout, + dtype=dtype, + image_shape=image_shape, + ) + elif name.startswith("resnet3d-"): + n_layer = int(name.split("-")[1]) + mod, params = relay.testing.resnet.get_workload( + num_layers=n_layer, + batch_size=batch_size, + layout=layout, + dtype=dtype, + image_shape=image_shape, + ) + elif name == "mobilenet": + mod, params = relay.testing.mobilenet.get_workload( + batch_size=batch_size, layout=layout, dtype=dtype, image_shape=image_shape + ) + elif name == "squeezenet_v1.1": + assert layout == "NCHW", "squeezenet_v1.1 only supports NCHW layout" + mod, params = relay.testing.squeezenet.get_workload( + version="1.1", + batch_size=batch_size, + dtype=dtype, + image_shape=image_shape, + ) + elif name == "inception_v3": + input_shape = (batch_size, 3, 299, 299) if layout == "NCHW" else (batch_size, 299, 299, 3) + mod, params = relay.testing.inception_v3.get_workload(batch_size=batch_size, dtype=dtype) + elif name == "mxnet": + # MXNet 模型的示例 + from mxnet.gluon.model_zoo.vision import get_model + + assert layout == "NCHW" + + block = get_model("resnet50_v1", pretrained=True) + mod, params = relay.frontend.from_mxnet(block, shape={"data": input_shape}, dtype=dtype) + net = mod["main"] + net = relay.Function( + net.params, relay.nn.softmax(net.body), None, net.type_params, net.attrs + ) + mod = tvm.IRModule.from_expr(net) + elif name == "mlp": + mod, params = relay.testing.mlp.get_workload( + batch_size=batch_size, dtype=dtype, image_shape=image_shape, num_classes=1000 + ) + else: + raise ValueError("Network not found.") + + if use_sparse: + from tvm.topi.sparse.utils import convert_model_dense_to_sparse + + mod, params = convert_model_dense_to_sparse(mod, params, random_params=True) + + return mod, params, input_shape, output_shape +``` + +## 启动 RPC 跟踪器 + +TVM 使用 RPC session 与 ARM 板进行通信。在调优期间,调优器会将生成的代码发送到板上并测试板上代码的速度。 + +为了加速调优,TVM 使用 RPC 跟踪器(集中的控制器节点)来管理分布式设备。例如,若有 10 部手机,可以将它们全部注册到跟踪器,并行运行 10 次测试,从而加快调优过程。 + +整个调优过程都需要跟踪器。因此需要为此命令打开一个新终端,在主机上运行如下命令启动 RPC 跟踪器: + +``` bash +python -m tvm.exec.rpc_tracker --host=0.0.0.0 --port=9190 +``` + +预期输出: + +``` bash +INFO:RPCTracker:bind to 0.0.0.0:9190 +``` + +## 将设备注册到 RPC 跟踪器 + +接下来把设备注册到跟踪器。第一步是为 ARM 设备构建 TVM runtime 。 + +* 对于 Linux:按照 [在设备上构建 TVM Runtime](https://tvm.apache.org/docs/how_to/deploy_models/deploy_model_on_rasp.html#build-tvm-runtime-on-device) 教程操作,然后将设备注册到 Tracker + + ``` bash + python -m tvm.exec.rpc_server --tracker=[HOST_IP]:9190 --key=rasp4b-64 + ``` + + (将 `[HOST_IP]` 换为你的主机的 IP 地址) + +* 对于 Android:按照此 [说明](https://github.com/apache/tvm/tree/main/apps/android_rpc) 在 Android 设备上安装 TVM RPC APK,确保可以通过 Android rpc 测试。在调优期间,打开手机开发者选项并勾选「在更改期间保持屏幕唤醒」,为手机接通电源。 + +注册设备后,通过查询 rpc_tracker 来确认是否注册成功 + +``` bash +python -m tvm.exec.query_rpc_tracker --host=0.0.0.0 --port=9190 +``` + +例如,如果有 2 个华为 mate10 pro,11 个 64 位操作系统的树莓派 4B,以及 2 个 rk3399,则输出可以是 + +``` bash +Queue Status +---------------------------------- +key total free pending +---------------------------------- +mate10pro 2 2 0 +rk3399 2 2 0 +rasp4b-64 11 11 0 +---------------------------------- +``` + +将多个设备注册到 tracker,从而加快调优测试。 + +## 设置调优配置 + +在调优之前,进行配置。这里以 Raspberry Pi 4b 4GB 板(64 位操作系统 Ubuntu 20.04)为例。若用 Android 手机,请将 `use_ndk` 设置为 True。 + +``` python +#### 设备配置 #### +# 将 "aarch64-linux-gnu" 替换为你的板子的正确 target。 +# 此 target 用于交叉编译。可以通过:code:`gcc -v` 来查询。 +# FIXME(tmoreau89, merrymercy): 将 '-device=arm_cpu' 排除在 target 字符串之外 +# 因为共享 x86 操作策略。 +target = tvm.target.Target("llvm -mtriple=aarch64-linux-gnu -mattr=+neon") + +# 替换为跟踪器中的 device_key、rpc 主机和 rpc 端口 +device_key = "rasp4b-64" +rpc_host = "127.0.0.1" +rpc_port = 9190 + +# 如果使用 ndk 工具进行交叉编译,则设置为 True +# 并且还要设置下面的环境变量指向交叉编译器 +use_ndk = False +# os.environ["TVM_NDK_CC"] = "/usr/bin/aarch64-linux-gnu-g++" + +#### 调优 OPTION #### +network = "mobilenet" +use_sparse = False +batch_size = 1 +layout = "NHWC" +dtype = "float32" +log_file = "%s-%s-B%d-%s.json" % (network, layout, batch_size, target.kind.name) +``` + +## 提取搜索任务 + +接下来,从网络中提取搜索任务及其权重。任务的权重是任务的子图在整个网络中出现的次数。通过使用权重,可以将网络的端到端延迟近似为 `sum(latency[t] * weight[t])`,其中 `latency[t]` 是任务的延迟,而`weight[t]` 是任务的权重,任务调度器只会优化这个目标。 + +``` python +# 从网络中提取任务 +print("Get model...") +mod, params, input_shape, output_shape = get_network( + network, batch_size, layout, dtype=dtype, use_sparse=use_sparse +) +print("Extract tasks...") +tasks, task_weights = auto_scheduler.extract_tasks(mod["main"], params, target) + +for idx, task in enumerate(tasks): + print("========== Task %d (workload key: %s) ==========" % (idx, task.workload_key)) + print(task.compute_dag) +``` + +输出结果: + +``` bash +Get model... +Extract tasks... +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +========== Task 0 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 7, 7, 1024], [1, 1, 1024, 1024], [1, 1, 1, 1024], [1, 7, 7, 1024]]) ========== +placeholder = PLACEHOLDER [1, 7, 7, 1024] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 1024, 1024] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 1024] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 1 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 14, 14, 256], [1, 1, 256, 512], [1, 1, 1, 512], [1, 14, 14, 512]]) ========== +placeholder = PLACEHOLDER [1, 14, 14, 256] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 256, 512] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 512] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 2 (workload key: ["06fce76bd84cb904eee50b905ca9449a", [1, 28, 28, 256], [3, 3, 256, 1], [1, 1, 1, 256], [1, 28, 28, 256]]) ========== +placeholder = PLACEHOLDER [1, 28, 28, 256] +PaddedInput(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 29)) && (i2 >= 1)) && (i2 < 29)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 256, 1] +DepthwiseConv2d(b, i, j, c) += (PaddedInput[b, (i + di), (j + dj), c]*placeholder[di, dj, c, 0]) +placeholder = PLACEHOLDER [1, 1, 1, 256] +T_add(ax0, ax1, ax2, ax3) = (DepthwiseConv2d[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 3 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 28, 28, 128], [1, 1, 128, 256], [1, 1, 1, 256], [1, 28, 28, 256]]) ========== +placeholder = PLACEHOLDER [1, 28, 28, 128] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 128, 256] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 256] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 4 (workload key: ["d7b65649a4dd54becea0a52aabbc5af5", [1, 1000], [1, 1000]]) ========== +placeholder = PLACEHOLDER [1, 1000] +T_softmax_maxelem(i0) max= placeholder[i0, k] +T_softmax_exp(i0, i1) = tir.exp((placeholder[i0, i1] - T_softmax_maxelem[i0])) +T_softmax_expsum(i0) += T_softmax_exp[i0, k] +T_softmax_norm(i0, i1) = (T_softmax_exp[i0, i1]/T_softmax_expsum[i0]) + +========== Task 5 (workload key: ["69115f188984ae34ede37c3b8ca40b43", [1, 7, 7, 1024], [1, 1, 1, 1024]]) ========== +placeholder = PLACEHOLDER [1, 7, 7, 1024] +tensor(ax0, ax1, ax2, ax3) += placeholder[ax0, ((ax1*7) + rv0), ((ax2*7) + rv1), ax3] +tensor(ax0, ax1, ax2, ax3) = (tensor[ax0, ax1, ax2, ax3]/(float32((select((bool)1, ((ax1 + 1)*7), (((ax1 + 1)*7) + 1)) - (ax1*7)))*float32((select((bool)1, ((ax2 + 1)*7), (((ax2 + 1)*7) + 1)) - (ax2*7))))) + +========== Task 6 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 7, 7, 512], [1, 1, 512, 1024], [1, 1, 1, 1024], [1, 7, 7, 1024]]) ========== +placeholder = PLACEHOLDER [1, 7, 7, 512] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 512, 1024] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 1024] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 7 (workload key: ["c87ba68bc180312f5716af09a77ca15b", [1, 56, 56, 128], [3, 3, 128, 1], [1, 1, 1, 128], [1, 28, 28, 128]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 128] +PaddedInput(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 57)) && (i2 >= 1)) && (i2 < 57)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 128, 1] +DepthwiseConv2d(b, i, j, c) += (PaddedInput[b, ((i*2) + di), ((j*2) + dj), c]*placeholder[di, dj, c, 0]) +placeholder = PLACEHOLDER [1, 1, 1, 128] +T_add(ax0, ax1, ax2, ax3) = (DepthwiseConv2d[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 8 (workload key: ["06fce76bd84cb904eee50b905ca9449a", [1, 7, 7, 1024], [3, 3, 1024, 1], [1, 1, 1, 1024], [1, 7, 7, 1024]]) ========== +placeholder = PLACEHOLDER [1, 7, 7, 1024] +PaddedInput(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 8)) && (i2 >= 1)) && (i2 < 8)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 1024, 1] +DepthwiseConv2d(b, i, j, c) += (PaddedInput[b, (i + di), (j + dj), c]*placeholder[di, dj, c, 0]) +placeholder = PLACEHOLDER [1, 1, 1, 1024] +T_add(ax0, ax1, ax2, ax3) = (DepthwiseConv2d[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 9 (workload key: ["c87ba68bc180312f5716af09a77ca15b", [1, 28, 28, 256], [3, 3, 256, 1], [1, 1, 1, 256], [1, 14, 14, 256]]) ========== +placeholder = PLACEHOLDER [1, 28, 28, 256] +PaddedInput(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 29)) && (i2 >= 1)) && (i2 < 29)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 256, 1] +DepthwiseConv2d(b, i, j, c) += (PaddedInput[b, ((i*2) + di), ((j*2) + dj), c]*placeholder[di, dj, c, 0]) +placeholder = PLACEHOLDER [1, 1, 1, 256] +T_add(ax0, ax1, ax2, ax3) = (DepthwiseConv2d[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 10 (workload key: ["c87ba68bc180312f5716af09a77ca15b", [1, 14, 14, 512], [3, 3, 512, 1], [1, 1, 1, 512], [1, 7, 7, 512]]) ========== +placeholder = PLACEHOLDER [1, 14, 14, 512] +PaddedInput(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 15)) && (i2 >= 1)) && (i2 < 15)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 512, 1] +DepthwiseConv2d(b, i, j, c) += (PaddedInput[b, ((i*2) + di), ((j*2) + dj), c]*placeholder[di, dj, c, 0]) +placeholder = PLACEHOLDER [1, 1, 1, 512] +T_add(ax0, ax1, ax2, ax3) = (DepthwiseConv2d[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 11 (workload key: ["c87ba68bc180312f5716af09a77ca15b", [1, 112, 112, 64], [3, 3, 64, 1], [1, 1, 1, 64], [1, 56, 56, 64]]) ========== +placeholder = PLACEHOLDER [1, 112, 112, 64] +PaddedInput(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 113)) && (i2 >= 1)) && (i2 < 113)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 64, 1] +DepthwiseConv2d(b, i, j, c) += (PaddedInput[b, ((i*2) + di), ((j*2) + dj), c]*placeholder[di, dj, c, 0]) +placeholder = PLACEHOLDER [1, 1, 1, 64] +T_add(ax0, ax1, ax2, ax3) = (DepthwiseConv2d[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 12 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 28, 28, 256], [1, 1, 256, 256], [1, 1, 1, 256], [1, 28, 28, 256]]) ========== +placeholder = PLACEHOLDER [1, 28, 28, 256] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 256, 256] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 256] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 13 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 56, 56, 128], [1, 1, 128, 128], [1, 1, 1, 128], [1, 56, 56, 128]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 128] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 128, 128] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 128] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 14 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 14, 14, 512], [1, 1, 512, 512], [1, 1, 1, 512], [1, 14, 14, 512]]) ========== +placeholder = PLACEHOLDER [1, 14, 14, 512] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 512, 512] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 512] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 15 (workload key: ["06fce76bd84cb904eee50b905ca9449a", [1, 112, 112, 32], [3, 3, 32, 1], [1, 1, 1, 32], [1, 112, 112, 32]]) ========== +placeholder = PLACEHOLDER [1, 112, 112, 32] +PaddedInput(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 113)) && (i2 >= 1)) && (i2 < 113)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 32, 1] +DepthwiseConv2d(b, i, j, c) += (PaddedInput[b, (i + di), (j + dj), c]*placeholder[di, dj, c, 0]) +placeholder = PLACEHOLDER [1, 1, 1, 32] +T_add(ax0, ax1, ax2, ax3) = (DepthwiseConv2d[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 16 (workload key: ["2ca148ecea6508ce625f85719021344f", [1, 224, 224, 3], [3, 3, 3, 32], [1, 112, 1, 1], [1, 112, 1, 1], [1, 112, 112, 32]]) ========== +placeholder = PLACEHOLDER [1, 224, 224, 3] +pad_temp(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 225)) && (i2 >= 1)) && (i2 < 225)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 3, 32] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, ((yy*2) + ry), ((xx*2) + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 112, 1, 1] +T_multiply(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3]*placeholder[ax0, ax1, 0, 0]) +placeholder = PLACEHOLDER [1, 112, 1, 1] +T_add(ax0, ax1, ax2, ax3) = (T_multiply[ax0, ax1, ax2, ax3] + placeholder[ax0, ax1, 0, 0]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 17 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 56, 56, 64], [1, 1, 64, 128], [1, 1, 1, 128], [1, 56, 56, 128]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 64] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 64, 128] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 128] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 18 (workload key: ["7d44c6e3c81cd80f61ff2265b2bae89a", [1, 1024], [1000, 1024], [1, 1000], [1, 1000]]) ========== +placeholder = PLACEHOLDER [1, 1024] +placeholder = PLACEHOLDER [1000, 1024] +T_matmul_NT(i, j) += (placeholder[i, k]*placeholder[j, k]) +placeholder = PLACEHOLDER [1, 1000] +T_add(ax0, ax1) = (T_matmul_NT[ax0, ax1] + placeholder[ax0, ax1]) + +========== Task 19 (workload key: ["06fce76bd84cb904eee50b905ca9449a", [1, 14, 14, 512], [3, 3, 512, 1], [1, 1, 1, 512], [1, 14, 14, 512]]) ========== +placeholder = PLACEHOLDER [1, 14, 14, 512] +PaddedInput(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 15)) && (i2 >= 1)) && (i2 < 15)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 512, 1] +DepthwiseConv2d(b, i, j, c) += (PaddedInput[b, (i + di), (j + dj), c]*placeholder[di, dj, c, 0]) +placeholder = PLACEHOLDER [1, 1, 1, 512] +T_add(ax0, ax1, ax2, ax3) = (DepthwiseConv2d[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 20 (workload key: ["06fce76bd84cb904eee50b905ca9449a", [1, 56, 56, 128], [3, 3, 128, 1], [1, 1, 1, 128], [1, 56, 56, 128]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 128] +PaddedInput(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 57)) && (i2 >= 1)) && (i2 < 57)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 128, 1] +DepthwiseConv2d(b, i, j, c) += (PaddedInput[b, (i + di), (j + dj), c]*placeholder[di, dj, c, 0]) +placeholder = PLACEHOLDER [1, 1, 1, 128] +T_add(ax0, ax1, ax2, ax3) = (DepthwiseConv2d[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 21 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 112, 112, 32], [1, 1, 32, 64], [1, 1, 1, 64], [1, 112, 112, 64]]) ========== +placeholder = PLACEHOLDER [1, 112, 112, 32] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 32, 64] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 64] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) +``` + +## 调优及评估 + +接下来为调优和启动搜索任务设置一些选项 + +* `num_measure_trials` 是调优期间可以使用的测试次数(根据自己的时间预算调整这个参数),若要进行快速演示,可将其设置为较小的数字(例如 200)。推荐将其设置为 `800 * len(tasks)` 左右,以便使搜索收敛。比如 resnet-50 有 29 个任务,所以可以设置为 20000。 +* 此外,使用 `RecordToFile` 将测试记录转储到日志文件中,测试记录可用于历史最佳查询、恢复搜索以及进行后续分析。 +* 更多参数参见 `auto_scheduler.TuningOptions`,`auto_scheduler.LocalRunner`。 + +自动调优后,可以用找到的最佳调度来编译网络。在自动调优期间,所有测试记录都被转储到日志文件中,可以读取日志文件加载最佳调度。 + +``` python +def tune_and_evaluate(): + print("Begin tuning...") + tuner = auto_scheduler.TaskScheduler(tasks, task_weights) + tune_option = auto_scheduler.TuningOptions( + num_measure_trials=200, # 将此更改为 20000 以达到最佳性能 + builder=auto_scheduler.LocalBuilder(build_func="ndk" if use_ndk else "default"), + runner=auto_scheduler.RPCRunner( + device_key, + host=rpc_host, + port=rpc_port, + timeout=30, + repeat=1, + min_repeat_ms=200, + enable_cpu_cache_flush=True, + ), + measure_callbacks=[auto_scheduler.RecordToFile(log_file)], + ) + tuner.tune(tune_option) + + # 用历史最佳编译 + print("Compile...") + with auto_scheduler.ApplyHistoryBest(log_file): + with tvm.transform.PassContext( + opt_level=3, config={"relay.backend.use_auto_scheduler": True} + ): + lib = relay.build(mod, target=target, params=params) + + # 导出库 + tmp = tempdir() + if use_ndk: + from tvm.contrib import ndk + + filename = "net.so" + lib.export_library(tmp.relpath(filename), ndk.create_shared) + else: + filename = "net.tar" + lib.export_library(tmp.relpath(filename)) + + # 上传模块到设备 + print("Upload...") + remote = auto_scheduler.utils.request_remote(device_key, rpc_host, rpc_port, timeout=10000) + remote.upload(tmp.relpath(filename)) + rlib = remote.load_module(filename) + + # 创建图执行器 + dev = remote.cpu() + module = graph_executor.GraphModule(rlib["default"](dev)) + data_tvm = tvm.nd.array((np.random.uniform(size=input_shape)).astype(dtype)) + module.set_input("data", data_tvm) + + # 评估 + print("Evaluate inference time cost...") + print(module.benchmark(dev, repeat=3, min_repeat_ms=500)) + +# 不在网页服务器中运行调优,因为服务器没有树莓派, +# 或正在运行的设备跟踪器。 +# 取消注释运行下面行。 +# tune_and_evaluate() +``` + +:::note +解释调优过程中打印的信息 + +在调优过程中,控制台上会打印很多用于调试的信息,最重要的信息是任务调度程序的输出,下表是输出示例。 + +``` bash +---------------------------------------------------------------------- +------------------------------ [ Task Scheduler ] +---------------------------------------------------------------------- +| ID | Latency (ms) | Speed (GFLOPS) | Trials | +------------------------------------------------- +| 0 | 0.013 | 0.31 | 64 | +| 1 | 0.845 | 2.43 | 448 | +| 2 | 0.046 | -0.00 | 64 | +| 3 | 4.194 | 24.53 | 2112 | +| 4 | 0.109 | 9.21 | 64 | +| 5 | 1.759 | 29.27 | 896 | +| 6 | 0.083 | 6.01 | 64 | +| 7 | 3.084 | 33.38 | 7680 | +| 8 | 0.136 | 14.78 | 384 | +| 9 | 1.349 | 38.23 | 768 | +| 10 | 0.133 | 7.55 | 128 | +| 11 | 2.747 | 37.56 | 1536 | +| 12 | 0.338 | 11.87 | 192 | +| 13 | 1.295 | 40.00 | 704 | +| 14 | 0.482 | 4.16 | 256 | +| 15 | 2.686 | 38.56 | 1344 | +| 16 | 0.884 | 9.08 | 448 | +| 17 | 1.332 | 39.18 | 704 | +| 18 | 1.045 | 3.84 | 576 | +| 19 | 1.391 | 38.09 | 704 | +| 20 | 0.777 | 10.34 | 448 | +| 21 | 0.739 | 30.97 | 448 | +------------------------------------------------- + Estimated total latency: 38.347 ms Trials: 19992 Used time : 19260 s Next ID: 3 +``` + +此表列出了所有任务的延迟和(预估)速度,还列出了所有任务的测试分配。最后一行打印了这些任务的总加权延迟,可以粗略估计网络的端到端执行时间。最后一行还打印了测试试验的总数、自动调优所花费的总时间以及下一个要调优的任务的 ID。 + +还有一些「dmlc::Error」错误,因为 auto-scheduler 会尝试一些无效的调度,若调优继续运行,则可以忽略这些错误,因为这些错误与主进程隔离。 +::: + +:::note +提前终止调优 + +可以通过强制终止此进程来提前终止调优,只要在日志文件中为每个任务获得至少一个有效的调度,就能够进行编译(下面的部分)。 +::: + +## 其他技巧 + +1. 在调优过程中,auto-scheduler 需要编译许多程序,并从中提取特征。这部分会占用大量 CPU 资源,所以推荐使用多核的高性能 CPU,加快搜索速度。 +2. 可以用 `python3 -m tvm.auto_scheduler.measure_record --mode distill -i log.json` 提取大日志文件,并仅保存最有用的记录。 +3. 可以从以前的日志文件恢复搜索,只需要在函数 `run_tuning` 中创建任务调度程序时添加一个新参数 `load_log_file`。比如,`tuner = auto_scheduler.TaskScheduler(tasks, task_weights, load_log_file=log_file)` +4. 若有多个 target CPU,则可以将所有这些 CPU 用于并行化测试。查看此 [部分](https://tvm.apache.org/docs/how_to/tune_with_autotvm/tune_relay_cuda.html#tutorials-autotvm-scale-up-rpc-tracker) 了解如何使用 RPC 跟踪器和 RPC 服务器。要在 auto-scheduler 中使用 RPC 跟踪器,请将 `TuningOptions` 中的 runner 替换为 `auto_scheduler.RPCRunner`。 + +[下载 Python 源代码:tune_network_arm.py](https://tvm.apache.org/docs/_downloads/17b139d609f9480c7eeda2da1f90f28c/tune_network_arm.py) + +[下载 Jupyter Notebook:tune_network_arm.ipynb](https://tvm.apache.org/docs/_downloads/4dc30a43f3a6aa3ed4bc3077ad35ff70/tune_network_arm.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/autoscheduler/05-autoschedule_mali.md b/versioned_docs/version-0.12.0/how_to/autoscheduler/05-autoschedule_mali.md new file mode 100644 index 00000000..927964a1 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/autoscheduler/05-autoschedule_mali.md @@ -0,0 +1,491 @@ +--- +title: 为 Mali GPU 自动调度神经网络 +--- + +# 为 Mali GPU 自动调度神经网络 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/tune_with_autoscheduler/tune_network_mali.html#sphx-glr-download-how-to-tune-with-autoscheduler-tune-network-mali-py) 下载完整的示例代码 +::: + +**作者**:[Zhao Wu](https://github.com/FrozenGene) + +针对特定设备和工作负载的自动调优对于获得最佳性能至关重要,本文介绍如何使用 auto-scheduler 为 Mali GPU 调优整个神经网络。 + +为自动调优神经网络,将网络划分为小的子图并独立调优。每个子图被视为一个搜索任务,任务调度器对时间进行切片,并动态地为这些任务分配时间资源,并预测每个任务对端到端执行时间的影响,优先考虑最能减少执行时间的任务。 + +对于每个子图,使用 `tvm/python/topi` 中的计算声明来获取张量表达式形式的计算 DAG。然后使用 auto-scheduler 来构建这个 DAG 的搜索空间,并搜索合适的调度(底层优化)。 + +与基于 template 的 [AutoTVM](/docs/how_to/autotune)(依赖手动 template 来定义搜索空间) 不同,auto-scheduler 不需要任何调度 template。换言之,auto-scheduler 只使用 `tvm/python/topi` 中的计算声明,不使用现有的调度 template。 + +注意,本教程无法在 Windows 或最新版本的 macOS 上运行。若要运行,需要将本教程的主体包装在 `if __name__ == "__main__":` 块中。 + +``` python +import numpy as np + +import tvm +from tvm import relay, auto_scheduler +import tvm.relay.testing +from tvm.contrib import graph_executor +import os +``` + +## 定义网络 + +首先,要用 Relay 前端 API 定义网络。可以从 `tvm.relay.testing` 加载一些预定义的网络。也可以从 MXNet、ONNX、PyTorch 和 TensorFlow 加载模型(参见 [前端教程](/docs/how_to/compile))。 + +对于卷积神经网络,尽管 auto-scheduler 可以在任何布局下正常运行,但通过 NHWC 布局实现的性能最佳。auto-scheduler 对 NHWC 布局进行了很多优化,因此推荐将模型转换为 NHWC 布局,从而得以使用 auto-scheduler。可用 [ConvertLayout](https://tvm.apache.org/docs/arch/convert_layout.html#convert-layout-usage) pass 在 TVM 中进行布局转换。 + +``` python +def get_network(name, batch_size, layout="NHWC", dtype="float32"): + """获取网络的符号定义和随机权重""" + + # auto-scheduler 更适合 NHWC 布局 + if layout == "NHWC": + image_shape = (224, 224, 3) + elif layout == "NCHW": + image_shape = (3, 224, 224) + else: + raise ValueError("Invalid layout: " + layout) + + input_shape = (batch_size,) + image_shape + output_shape = (batch_size, 1000) + + if name.startswith("resnet-"): + n_layer = int(name.split("-")[1]) + mod, params = relay.testing.resnet.get_workload( + num_layers=n_layer, + batch_size=batch_size, + layout=layout, + dtype=dtype, + image_shape=image_shape, + ) + elif name.startswith("resnet3d-"): + n_layer = int(name.split("-")[1]) + mod, params = relay.testing.resnet.get_workload( + num_layers=n_layer, + batch_size=batch_size, + layout=layout, + dtype=dtype, + image_shape=image_shape, + ) + elif name == "mobilenet": + mod, params = relay.testing.mobilenet.get_workload( + batch_size=batch_size, layout=layout, dtype=dtype, image_shape=image_shape + ) + elif name == "squeezenet_v1.1": + assert layout == "NCHW", "squeezenet_v1.1 only supports NCHW layout" + mod, params = relay.testing.squeezenet.get_workload( + version="1.1", + batch_size=batch_size, + dtype=dtype, + image_shape=image_shape, + ) + elif name == "inception_v3": + input_shape = (batch_size, 3, 299, 299) if layout == "NCHW" else (batch_size, 299, 299, 3) + mod, params = relay.testing.inception_v3.get_workload(batch_size=batch_size, dtype=dtype) + elif name == "mxnet": + # MXNet 模型的示例 + from mxnet.gluon.model_zoo.vision import get_model + + assert layout == "NCHW" + + block = get_model("resnet50_v1", pretrained=True) + mod, params = relay.frontend.from_mxnet(block, shape={"data": input_shape}, dtype=dtype) + net = mod["main"] + net = relay.Function( + net.params, relay.nn.softmax(net.body), None, net.type_params, net.attrs + ) + mod = tvm.IRModule.from_expr(net) + + return mod, params, input_shape, output_shape + +# 定义神经网络和编译 target。 +network = "mobilenet" +batch_size = 1 +layout = "NHWC" +# 如果使用 ndk 工具进行交叉编译,则设置为 True +use_ndk = True +# 交叉编译器路径 +os.environ["TVM_NDK_CC"] = "/usr/bin/aarch64-linux-gnu-g++" +target = tvm.target.Target("opencl -device=mali", host="llvm -mtriple=aarch64-linux-gnu") +dtype = "float32" +log_file = "%s-%s-B%d-%s.json" % (network, layout, batch_size, target.kind.name) +``` + +## 启动 RPC 跟踪器并将设备注册到跟踪器 + +参考本 [教程](/docs/how_to/autotune/autotuning_mobile) 中的「启动 RPC 跟踪器」和「将设备注册到 RPC 跟踪器」章节来启动 RPC 跟踪器,并将设备注册到跟踪器。 + +``` python +# 将其替换为跟踪器中的设备密钥 +device_key = "rk3399" +``` + +## 提取搜索任务 + +接下来,从网络中提取搜索任务及其权重。任务的权重是任务的子图在整个网络中出现的次数。通过使用权重,可以将网络的端到端延迟近似为 `sum(latency[t] * weight[t])`,其中 `latency[t]` 是任务的延迟,而 `weight[t]` 是任务的权重,任务调度器仅针对该目标进行优化。 + +``` python +# 从网络中提取任务 +print("Extract tasks...") +mod, params, input_shape, output_shape = get_network(network, batch_size, layout, dtype=dtype) +tasks, task_weights = auto_scheduler.extract_tasks(mod["main"], params, target) + +for idx, task in enumerate(tasks): + print("========== Task %d (workload key: %s) ==========" % (idx, task.workload_key)) + print(task.compute_dag) +``` + +输出结果: + +``` bash +Extract tasks... +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +========== Task 0 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 7, 7, 1024], [1, 1, 1024, 1024], [1, 1, 1, 1024], [1, 7, 7, 1024]]) ========== +placeholder = PLACEHOLDER [1, 7, 7, 1024] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 1024, 1024] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 1024] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 1 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 14, 14, 256], [1, 1, 256, 512], [1, 1, 1, 512], [1, 14, 14, 512]]) ========== +placeholder = PLACEHOLDER [1, 14, 14, 256] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 256, 512] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 512] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 2 (workload key: ["06fce76bd84cb904eee50b905ca9449a", [1, 28, 28, 256], [3, 3, 256, 1], [1, 1, 1, 256], [1, 28, 28, 256]]) ========== +placeholder = PLACEHOLDER [1, 28, 28, 256] +PaddedInput(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 29)) && (i2 >= 1)) && (i2 < 29)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 256, 1] +DepthwiseConv2d(b, i, j, c) += (PaddedInput[b, (i + di), (j + dj), c]*placeholder[di, dj, c, 0]) +placeholder = PLACEHOLDER [1, 1, 1, 256] +T_add(ax0, ax1, ax2, ax3) = (DepthwiseConv2d[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 3 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 28, 28, 128], [1, 1, 128, 256], [1, 1, 1, 256], [1, 28, 28, 256]]) ========== +placeholder = PLACEHOLDER [1, 28, 28, 128] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 128, 256] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 256] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 4 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 7, 7, 512], [1, 1, 512, 1024], [1, 1, 1, 1024], [1, 7, 7, 1024]]) ========== +placeholder = PLACEHOLDER [1, 7, 7, 512] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 512, 1024] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 1024] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 5 (workload key: ["69115f188984ae34ede37c3b8ca40b43", [1, 7, 7, 1024], [1, 1, 1, 1024]]) ========== +placeholder = PLACEHOLDER [1, 7, 7, 1024] +tensor(ax0, ax1, ax2, ax3) += placeholder[ax0, ((ax1*7) + rv0), ((ax2*7) + rv1), ax3] +tensor(ax0, ax1, ax2, ax3) = (tensor[ax0, ax1, ax2, ax3]/(float32((select((bool)1, ((ax1 + 1)*7), (((ax1 + 1)*7) + 1)) - (ax1*7)))*float32((select((bool)1, ((ax2 + 1)*7), (((ax2 + 1)*7) + 1)) - (ax2*7))))) + +========== Task 6 (workload key: ["d7b65649a4dd54becea0a52aabbc5af5", [1, 1000], [1, 1000]]) ========== +placeholder = PLACEHOLDER [1, 1000] +T_softmax_maxelem(i0) max= placeholder[i0, k] +T_softmax_exp(i0, i1) = tir.exp((placeholder[i0, i1] - T_softmax_maxelem[i0])) +T_softmax_expsum(i0) += T_softmax_exp[i0, k] +T_softmax_norm(i0, i1) = (T_softmax_exp[i0, i1]/T_softmax_expsum[i0]) + +========== Task 7 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 56, 56, 128], [1, 1, 128, 128], [1, 1, 1, 128], [1, 56, 56, 128]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 128] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 128, 128] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 128] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 8 (workload key: ["06fce76bd84cb904eee50b905ca9449a", [1, 7, 7, 1024], [3, 3, 1024, 1], [1, 1, 1, 1024], [1, 7, 7, 1024]]) ========== +placeholder = PLACEHOLDER [1, 7, 7, 1024] +PaddedInput(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 8)) && (i2 >= 1)) && (i2 < 8)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 1024, 1] +DepthwiseConv2d(b, i, j, c) += (PaddedInput[b, (i + di), (j + dj), c]*placeholder[di, dj, c, 0]) +placeholder = PLACEHOLDER [1, 1, 1, 1024] +T_add(ax0, ax1, ax2, ax3) = (DepthwiseConv2d[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 9 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 56, 56, 64], [1, 1, 64, 128], [1, 1, 1, 128], [1, 56, 56, 128]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 64] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 64, 128] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 128] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 10 (workload key: ["c87ba68bc180312f5716af09a77ca15b", [1, 14, 14, 512], [3, 3, 512, 1], [1, 1, 1, 512], [1, 7, 7, 512]]) ========== +placeholder = PLACEHOLDER [1, 14, 14, 512] +PaddedInput(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 15)) && (i2 >= 1)) && (i2 < 15)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 512, 1] +DepthwiseConv2d(b, i, j, c) += (PaddedInput[b, ((i*2) + di), ((j*2) + dj), c]*placeholder[di, dj, c, 0]) +placeholder = PLACEHOLDER [1, 1, 1, 512] +T_add(ax0, ax1, ax2, ax3) = (DepthwiseConv2d[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 11 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 28, 28, 256], [1, 1, 256, 256], [1, 1, 1, 256], [1, 28, 28, 256]]) ========== +placeholder = PLACEHOLDER [1, 28, 28, 256] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 256, 256] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 256] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 12 (workload key: ["06fce76bd84cb904eee50b905ca9449a", [1, 56, 56, 128], [3, 3, 128, 1], [1, 1, 1, 128], [1, 56, 56, 128]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 128] +PaddedInput(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 57)) && (i2 >= 1)) && (i2 < 57)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 128, 1] +DepthwiseConv2d(b, i, j, c) += (PaddedInput[b, (i + di), (j + dj), c]*placeholder[di, dj, c, 0]) +placeholder = PLACEHOLDER [1, 1, 1, 128] +T_add(ax0, ax1, ax2, ax3) = (DepthwiseConv2d[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 13 (workload key: ["c87ba68bc180312f5716af09a77ca15b", [1, 112, 112, 64], [3, 3, 64, 1], [1, 1, 1, 64], [1, 56, 56, 64]]) ========== +placeholder = PLACEHOLDER [1, 112, 112, 64] +PaddedInput(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 113)) && (i2 >= 1)) && (i2 < 113)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 64, 1] +DepthwiseConv2d(b, i, j, c) += (PaddedInput[b, ((i*2) + di), ((j*2) + dj), c]*placeholder[di, dj, c, 0]) +placeholder = PLACEHOLDER [1, 1, 1, 64] +T_add(ax0, ax1, ax2, ax3) = (DepthwiseConv2d[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 14 (workload key: ["06fce76bd84cb904eee50b905ca9449a", [1, 112, 112, 32], [3, 3, 32, 1], [1, 1, 1, 32], [1, 112, 112, 32]]) ========== +placeholder = PLACEHOLDER [1, 112, 112, 32] +PaddedInput(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 113)) && (i2 >= 1)) && (i2 < 113)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 32, 1] +DepthwiseConv2d(b, i, j, c) += (PaddedInput[b, (i + di), (j + dj), c]*placeholder[di, dj, c, 0]) +placeholder = PLACEHOLDER [1, 1, 1, 32] +T_add(ax0, ax1, ax2, ax3) = (DepthwiseConv2d[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 15 (workload key: ["c87ba68bc180312f5716af09a77ca15b", [1, 56, 56, 128], [3, 3, 128, 1], [1, 1, 1, 128], [1, 28, 28, 128]]) ========== +placeholder = PLACEHOLDER [1, 56, 56, 128] +PaddedInput(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 57)) && (i2 >= 1)) && (i2 < 57)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 128, 1] +DepthwiseConv2d(b, i, j, c) += (PaddedInput[b, ((i*2) + di), ((j*2) + dj), c]*placeholder[di, dj, c, 0]) +placeholder = PLACEHOLDER [1, 1, 1, 128] +T_add(ax0, ax1, ax2, ax3) = (DepthwiseConv2d[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 16 (workload key: ["2ca148ecea6508ce625f85719021344f", [1, 224, 224, 3], [3, 3, 3, 32], [1, 112, 1, 1], [1, 112, 1, 1], [1, 112, 112, 32]]) ========== +placeholder = PLACEHOLDER [1, 224, 224, 3] +pad_temp(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 225)) && (i2 >= 1)) && (i2 < 225)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 3, 32] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, ((yy*2) + ry), ((xx*2) + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 112, 1, 1] +T_multiply(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3]*placeholder[ax0, ax1, 0, 0]) +placeholder = PLACEHOLDER [1, 112, 1, 1] +T_add(ax0, ax1, ax2, ax3) = (T_multiply[ax0, ax1, ax2, ax3] + placeholder[ax0, ax1, 0, 0]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 17 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 14, 14, 512], [1, 1, 512, 512], [1, 1, 1, 512], [1, 14, 14, 512]]) ========== +placeholder = PLACEHOLDER [1, 14, 14, 512] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 512, 512] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 512] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 18 (workload key: ["7d44c6e3c81cd80f61ff2265b2bae89a", [1, 1024], [1000, 1024], [1, 1000], [1, 1000]]) ========== +placeholder = PLACEHOLDER [1, 1024] +placeholder = PLACEHOLDER [1000, 1024] +T_matmul_NT(i, j) += (placeholder[i, k]*placeholder[j, k]) +placeholder = PLACEHOLDER [1, 1000] +T_add(ax0, ax1) = (T_matmul_NT[ax0, ax1] + placeholder[ax0, ax1]) + +========== Task 19 (workload key: ["06fce76bd84cb904eee50b905ca9449a", [1, 14, 14, 512], [3, 3, 512, 1], [1, 1, 1, 512], [1, 14, 14, 512]]) ========== +placeholder = PLACEHOLDER [1, 14, 14, 512] +PaddedInput(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 15)) && (i2 >= 1)) && (i2 < 15)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 512, 1] +DepthwiseConv2d(b, i, j, c) += (PaddedInput[b, (i + di), (j + dj), c]*placeholder[di, dj, c, 0]) +placeholder = PLACEHOLDER [1, 1, 1, 512] +T_add(ax0, ax1, ax2, ax3) = (DepthwiseConv2d[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 20 (workload key: ["c87ba68bc180312f5716af09a77ca15b", [1, 28, 28, 256], [3, 3, 256, 1], [1, 1, 1, 256], [1, 14, 14, 256]]) ========== +placeholder = PLACEHOLDER [1, 28, 28, 256] +PaddedInput(i0, i1, i2, i3) = tir.if_then_else(((((i1 >= 1) && (i1 < 29)) && (i2 >= 1)) && (i2 < 29)), placeholder[i0, (i1 - 1), (i2 - 1), i3], 0f) +placeholder = PLACEHOLDER [3, 3, 256, 1] +DepthwiseConv2d(b, i, j, c) += (PaddedInput[b, ((i*2) + di), ((j*2) + dj), c]*placeholder[di, dj, c, 0]) +placeholder = PLACEHOLDER [1, 1, 1, 256] +T_add(ax0, ax1, ax2, ax3) = (DepthwiseConv2d[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) + +========== Task 21 (workload key: ["1037be767e8e18197e87653d81c34558", [1, 112, 112, 32], [1, 1, 32, 64], [1, 1, 1, 64], [1, 112, 112, 64]]) ========== +placeholder = PLACEHOLDER [1, 112, 112, 32] +pad_temp(i0, i1, i2, i3) = placeholder[i0, i1, i2, i3] +placeholder = PLACEHOLDER [1, 1, 32, 64] +conv2d_nhwc(nn, yy, xx, ff) += (pad_temp[nn, (yy + ry), (xx + rx), rc]*placeholder[ry, rx, rc, ff]) +placeholder = PLACEHOLDER [1, 1, 1, 64] +T_add(ax0, ax1, ax2, ax3) = (conv2d_nhwc[ax0, ax1, ax2, ax3] + placeholder[ax0, 0, 0, ax3]) +T_relu(ax0, ax1, ax2, ax3) = max(T_add[ax0, ax1, ax2, ax3], 0f) +``` + +:::note +如何从远程设备获取硬件参数 + +``` python +from tvm.auto_scheduler.utils import request_remote +remote = request_remote(device_key, "127.0.0.1", 9190) +dev = remote.cl() +max_shared_memory_per_block = dev.max_shared_memory_per_block +# 没有明确的本地内存限制 +# 可以使用 INT32_MAX 来禁用对 local_memory 的检查。 +max_local_memory_per_block = 2147483647 # INT32_MAX +max_threads_per_block = dev.max_threads_per_block +max_vthread_extent = int(dev.warp_size / 4) if int(dev.warp_size / 4) 1 else dev.warp_size +warp_size = dev.warp_size +hardware_params = auto_scheduler.HardwareParams(-1, 16, 64, + max_shared_memory_per_block, max_local_memory_per_block, + max_threads_per_block, max_vthread_extent, warp_size) +``` + +接下来可以将其传递给搜索任务,并进行调优。 + +``` python +tasks, task_weights = auto_scheduler.extract_tasks( + mod["main"], params, target, hardware_params = hardware_params +) +``` +::: + +## 调优及评估 + +接下来设置一些调优选项,启动搜索任务,并评估端到端性能 + +* `num_measure_trials` 是调优期间可以使用的测试次数(根据自己的时间预算调整这个参数),若要进行快速演示,可将其设置为较小的数字(例如 200)。推荐将其设置为 `800 * len(tasks)` 左右,以便使搜索收敛。比如 resnet-50 有 29 个任务,所以可以设置为 20000。 +* 此外,使用 `RecordToFile` 将测试记录转储到日志文件中,测试记录可用于历史最佳查询、恢复搜索以及进行后续分析。 +* 更多参数参见 `auto_scheduler.TuningOptions`,`auto_scheduler.LocalRunner`。 + +``` python +def tune_and_evaluate(): + print("Begin tuning...") + tuner = auto_scheduler.TaskScheduler(tasks, task_weights) + tune_option = auto_scheduler.TuningOptions( + num_measure_trials=200, # 将此更改为 20000 以达到最佳性能 + builder=auto_scheduler.LocalBuilder(build_func="ndk" if use_ndk else "default"), + runner=auto_scheduler.RPCRunner( + device_key, host="127.0.0.1", port=9190, repeat=3, timeout=50 + ), + measure_callbacks=[auto_scheduler.RecordToFile(log_file)], + ) + tuner.tune(tune_option) + + # 编译整个网络 + print("Compile...") + with auto_scheduler.ApplyHistoryBest(log_file): + with tvm.transform.PassContext( + opt_level=3, config={"relay.backend.use_auto_scheduler": True} + ): + lib = relay.build(mod, target, params=params) + + # 创建图执行器 + print("=============== Request Remote ===============") + from tvm.auto_scheduler.utils import request_remote + + remote = request_remote(device_key, "127.0.0.1", 9190) + dev = remote.cl() + from tvm.contrib import utils, ndk + + temp = utils.tempdir() + filename = "deploy_lib.so" + path_lib = temp.relpath(filename) + lib.export_library(path_lib, ndk.create_shared) + remote.upload(path_lib) + loaded_lib = remote.load_module(filename) + module = graph_executor.GraphModule(loaded_lib["default"](dev)) + data = (np.random.uniform(size=input_shape)).astype(dtype) + data_tvm = tvm.nd.array(data) + module.set_input("data", data_tvm) + + # 评估 + print("Evaluate inference time cost...") + print(module.benchmark(dev, repeat=3, min_repeat_ms=500)) + + + +# 不在网页服务器中运行调优,因为它需要的时间太长。 +# 取消注释运行以下行。 +# tune_and_evaluate() +``` + +:::note +解释调优过程中打印的信息 + +在调优过程中,控制台上会打印很多用于调试的信息,最重要的信息是任务调度程序的输出,下表是输出示例。 + +``` bash +---------------------------------------------------------------------- +------------------------------ [ Task Scheduler ] +---------------------------------------------------------------------- +| ID | Latency (ms) | Speed (GFLOPS) | Trials | +------------------------------------------------- +| 0 | 0.010 | 0.40 | 64 | +| 1 | 0.087 | 47.19 | 64 | +| 2 | 0.008 | -0.00 | 64 | +| 3 | 0.177 | 582.07 | 64 | +| 4 | 0.268 | 862.37 | 256 | +| 5 | 0.166 | 621.13 | 128 | +| 6 | 0.170 | 605.10 | 128 | +| 7 | 0.128 | 403.20 | 64 | +| 8 | 0.189 | 545.71 | 64 | +| 9 | 0.231 | 1001.01 | 448 | +| 10 | 0.155 | 664.80 | 256 | +| 11 | 0.155 | 662.86 | 256 | +| 12 | 0.119 | 434.08 | 64 | +| 13 | 0.199 | 522.13 | 64 | +| 14 | 0.235 | 986.56 | 320 | +| 15 | 0.149 | 689.13 | 128 | +| 16 | 0.155 | 664.80 | 192 | +| 17 | 0.151 | 340.64 | 64 | +| 18 | 0.176 | 597.55 | 128 | +| 19 | 0.220 | 1054.37 | 192 | +| 20 | 0.150 | 686.01 | 128 | +| 21 | 0.159 | 650.88 | 128 | +| 22 | 0.073 | 358.19 | 64 | +| 23 | 0.031 | 70.63 | 64 | +| 24 | 0.251 | 947.73 | 128 | +| 25 | 0.157 | 652.47 | 128 | +| 26 | 0.215 | 954.84 | 128 | +| 27 | 0.237 | 868.92 | 128 | +| 28 | 0.266 | 774.06 | 128 | +------------------------------------------------- +Estimated total latency: 10.016 ms Trials: 3992 Used time : 1131 s Next ID: 15 +``` + +此表列出了所有任务的延迟和(预估)速度,还列出了所有任务的测试分配。最后一行打印了这些任务的总加权延迟,可以粗略估计网络的端到端执行时间。最后一行还打印了测试的总数、自动调优所花费的总时间以及下一个要调优的任务的 ID。 + +还有一些「tvm::Error」错误,因为 auto-scheduler 会尝试一些无效的调度。若调优继续运行,则可以忽略这些错误,因为这些错误与主进程隔离。 +::: + +:::note +提前终止调优 + +可以通过强制终止此进程来提前终止调优,只要在日志文件中为每个任务获得至少一个有效的调度,就能够进行编译(下面的部分)。 +::: + +## 其他技巧 + +1. 在调优过程中,auto-scheduler 需要编译许多程序,并从中提取特征。这部分会占用大量 CPU 资源,所以推荐使用多核的高性能 CPU,加快搜索速度。 +2. 可以使用 `python3 -m tvm.auto_scheduler.measure_record --mode distill -i log.json` 提取大日志文件,并仅保存最有用的记录。 +3. 可以从以前的日志文件恢复搜索,只需要在函数 `run_tuning` 中创建任务调度程序时添加一个新参数 `load_log_file`。比如,`tuner = auto_scheduler.TaskScheduler(tasks, task_weights, load_log_file=log_file)` +4. 若有多个 target CPU,则可以将所有这些 CPU 用于并行化测试。查看这 [部分](https://tvm.apache.org/docs/how_to/tune_with_autotvm/tune_relay_cuda.html#tutorials-autotvm-scale-up-rpc-tracker) 了解如何使用 RPC 跟踪器和 RPC 服务器。要在 auto-scheduler 中使用 RPC 跟踪器,请将 `TuningOptions` 中的 runner 替换为 `auto_scheduler.RPCRunner`。 + +[下载 Python 源代码:tune_network_mali.py](https://tvm.apache.org/docs/_downloads/67bf7dd99bcfb837cf3e8b461a5eeb48/tune_network_mali.py) + +[下载 Jupyter Notebook:tune_network_mali.ipynb](https://tvm.apache.org/docs/_downloads/5e4e499c097b16a90c517e630502253a/tune_network_mali.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/autoscheduler/06-autoschedule_custom.md b/versioned_docs/version-0.12.0/how_to/autoscheduler/06-autoschedule_custom.md new file mode 100644 index 00000000..98a80e64 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/autoscheduler/06-autoschedule_custom.md @@ -0,0 +1,611 @@ +--- +title: 使用自定义调度规则(Sketch Rule)在 CPU 上自动调度稀疏矩阵乘法 +--- + +# 使用自定义调度规则(Sketch Rule)在 CPU 上自动调度稀疏矩阵乘法 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/tune_with_autoscheduler/tune_sparse_x86.html#sphx-glr-download-how-to-tune-with-autoscheduler-tune-sparse-x86-py) 下载完整的示例代码 +::: + +**作者**:[Chengfan Jia](https://github.com/jcf94/) + +本文介绍如何用 auto-scheduler 来调优 CPU 的稀疏矩阵乘法。 + +auto-scheduler 旨在自动探索给定计算声明的最佳性能调度。有时需要尝试一些特殊的操作,auto-scheduler 的默认调度规则(Sketch Rule)可能不能很好的支持这些操作,会导致性能不佳。auto-scheduler 目前允许用户提供一个 CustomSketch 来覆盖这些情况。 + +本教程使用稀疏矩阵乘法作为示例,演示如何实现自定义调度规则,并将其插入 auto-scheduler 的搜索策略。 + +注意,本教程无法在 Windows 或最新版本的 macOS 上运行。如需运行,请将本教程的主体放在 `if __name__ == "__main__":` 代码块中。 + +``` python +import os +import numpy as np + +import tvm +import tvm.testing +from tvm import te, auto_scheduler, runtime, topi +from tvm.auto_scheduler import _ffi_api +from tvm.topi.utils import get_const_tuple +from tvm.topi.sparse.utils import random_bsr_matrix +``` + +## 定义计算 + +首先用几个 relu 和 bias 相加来定义一个稀疏 matmul 的计算,该函数返回输入/输出张量列表,auto-scheduler 可以从这些张量中得到整个计算图。 + +``` python +@auto_scheduler.register_workload +def sparse_dense(M, N, K, w_data_shape, w_indices_shape, w_indptr_shape, dtype): + X = te.placeholder(shape=(M, K), dtype=dtype) + W_data = te.placeholder(shape=w_data_shape, dtype=dtype) + W_indices = te.placeholder(shape=w_indices_shape, dtype="int32") + W_indptr = te.placeholder(shape=w_indptr_shape, dtype="int32") + B = te.placeholder(shape=(M, N), dtype=dtype) + + out = topi.nn.sparse_dense(topi.nn.relu(X), W_data, W_indices, W_indptr) + out = te.compute((M, N), lambda i, j: out[i, j] + B[i, j], name="BiasAdd") + out = topi.nn.relu(out) + + return [X, W_data, W_indices, W_indptr, B, out] +``` + +## 稀疏工作负载(sparse workload)的特殊步骤 + +在调度调优期间,auto-scheduler 使用随机输入来测试生成的调度的性能。虽然不能直接使用随机数组作为稀疏运算的输入,但「indices」和「indptr」数组对于计算很有用。 + +为解决这个问题,将它们注册为特殊的 buffer,然后在测试程序时加载。更多详细信息,参阅 *tvm.auto_scheduler.measure.py* 。 + +``` python +# 定义稀疏计算的基本 shape +M = 128 +K = 256 +N = 512 +BS_R = 16 +BS_C = 1 +density = 0.6 + +# 用 numpy 生成测试数据 +X_np = np.random.randn(M, K).astype("float32") +X_np = np.maximum(np.zeros((M, K), dtype="float32"), X_np) # Relu +W_sp_np = random_bsr_matrix(N, K, BS_R, BS_C, density=density, dtype="float32") +W_np = W_sp_np.todense() +Y_np = X_np @ W_np.T # 处理矩阵乘法 +B_np = np.random.randn(M, N).astype("float32") +Y_np = Y_np + B_np # Bias add +Y_np = np.maximum(np.zeros((M, N), dtype="float32"), Y_np) # Relu +``` + +## 创建搜索任务 + +接下来创建一个搜索任务 M=N=K=512,dtype=「float32」。如果你的机器支持 avx 指令,你可以: + +* 将下面的「llvm」替换为「llvm -mcpu=core-avx2」来启用 AVX2 +* 将下面的「llvm」替换为「llvm -mcpu=skylake-avx512」来启用 AVX-512 + +``` python +target = tvm.target.Target("llvm") + +# 将稀疏数据注册到任务输入 +prefix = "sparse_dense_bsr_%d_%d_%d_%d_%d_%d_" % ( + N, + K, + BS_R, + BS_C, + W_sp_np.indices.shape[0], + W_sp_np.indptr.shape[0], +) +task = tvm.auto_scheduler.SearchTask( + func=sparse_dense, + args=(M, N, K, W_sp_np.data.shape, W_sp_np.indices.shape, W_sp_np.indptr.shape, "float32"), + target=target, + task_inputs={ + prefix + "W_data": runtime.ndarray.array(W_sp_np.data), + prefix + "W_indices": runtime.ndarray.array(W_sp_np.indices), + prefix + "W_indptr": runtime.ndarray.array(W_sp_np.indptr), + }, + task_inputs_save_to_file=True, +) + +# 检查计算图 +print("Computational DAG:") +print(task.compute_dag) +``` + +输出结果: + +``` bash +Computational DAG: +placeholder = PLACEHOLDER [33] +placeholder = PLACEHOLDER [4916, 16, 1] +placeholder = PLACEHOLDER [4916] +placeholder = PLACEHOLDER [128, 256] +compute(i0, i1) = max(placeholder[i0, i1], 0f) +compute(i, nb_j, j) += (placeholder[(placeholder[nb_j] + elem_idx), j, c]*compute[i, (placeholder[(placeholder[nb_j] + elem_idx)] + c)]) +compute(m, n) = compute[m, floordiv(n, 16), floormod(n, 16)] +placeholder = PLACEHOLDER [128, 512] +BiasAdd(i, j) = (compute[i, j] + placeholder[i, j]) +compute(i0, i1) = max(BiasAdd[i0, i1], 0f) +``` + +## 为稀疏密集算子(sparse dense op)编写自定义草图(sketch) + +在调优之前,需要为稀疏密集操作定义 CustomSketchRule。 + +CustomSketchRule 由两部分组成:条件函数和应用函数。 + +* 条件函数:描述应用此调度规则的时间。例如,通过匹配名称和标签将规则应用于稀疏操作。 +* 应用函数:描述生成初始草图的方式。可以用 auto-scheduler 提供的循环状态 API 来实现。 + +``` python +def meet_condition_func(search_policy, state, stage_id): + state = auto_scheduler.loop_state.State(state, search_policy.search_task.compute_dag) + if state.stages[stage_id].op.tag in [ + "sparse_dense_sp_rhs_bsrmm", + "sparse_dense_sp_rhs_bsrmm_block", + ]: + return auto_scheduler.PreloadCustomSketchRule.APPLY_AND_SKIP_REST + else: + return auto_scheduler.PreloadCustomSketchRule.PASS + +def apply_func(search_policy, state, stage_id): + ret = [] + s0 = auto_scheduler.loop_state.State(state, search_policy.search_task.compute_dag) + if s0.stages[stage_id].op.tag == "sparse_dense_sp_rhs_bsrmm_block": + return [s0.state_object, stage_id - 1] + + sparse_dense = s0.stages[stage_id].op + sparse_dense_block = s0.stages[stage_id - 1].op + assert sparse_dense.tag == "sparse_dense_sp_rhs_bsrmm" + assert sparse_dense_block.tag == "sparse_dense_sp_rhs_bsrmm_block" + + # 设置计算块的默认消费者 + consumer = sparse_dense + + # 若稀疏密集有单个元素消费者 + # 可以计算内联稀疏密集输出阶段 + consumers = _ffi_api.SearchPolicyUtilsGetConsumers( + search_policy.search_task, s0.state_object, stage_id + ) + if len(consumers) == 1: + consumer_id = int(consumers.items()[0][0]) + if _ffi_api.SearchPolicyUtilsIsElementwiseMatch( + search_policy.search_task, s0.state_object, stage_id, consumer_id + ): + consumer = s0.stages[consumer_id].op + s0.compute_inline(sparse_dense) + + i, nb_j, j, row_offset, c = s0[sparse_dense_block].iters + m, n = s0[consumer].iters + i0, i1, i2 = s0.split(sparse_dense_block, i, [None, None]) + m0, m1 = s0.follow_split(consumer, m, len(s0.transform_steps) - 1, 1) + j0, j1 = s0.split(sparse_dense_block, nb_j, [None]) + n0, n1 = s0.follow_split(consumer, n, len(s0.transform_steps) - 1, 1) + s0.reorder(sparse_dense_block, [i0, j0, i1, j1, row_offset, i2, j, c]) + s0.reorder(consumer, [m0, n0, m1, n1]) + s0.compute_at(sparse_dense_block, consumer, n0) + + ret.append([s0.state_object, stage_id - 2]) + + return ret +``` + +接下来,为插入自定义草图的 auto-scheduler 设置参数。 + +* `num_measure_trials` 是搜索过程中可以使用的测试次数(根据自己的时间预算调整这个参数),为快速演示,本教程只进行了 10 次试验。在实践中,推荐使用 1000 以得到收敛结果。 +* 此外,使用 `RecordToFile` 将测试记录转储到 *sparse_dense.json* 文件中,测试记录可用于查询历史最佳、恢复搜索以及以后进行更多分析。 +* 有关更多参数,参见 `auto_scheduler.TuningOptions` +* 接下来创建一个 `auto_scheduler.SketchPolicy` 对象,并将自定义调度规则添加为 *init_search_callbacks*。 + +``` python +log_file = "sparse_dense.json" +tune_option = auto_scheduler.TuningOptions( + num_measure_trials=10, + measure_callbacks=[auto_scheduler.RecordToFile(log_file)], + verbose=2, +) + +search_policy = auto_scheduler.SketchPolicy( + task, + program_cost_model=auto_scheduler.XGBModel(), + init_search_callbacks=[ + auto_scheduler.PreloadCustomSketchRule(meet_condition_func, apply_func, "SparseDense") + ], +) +``` + +## 运行搜索 + +现在已经准备好所有的输入。接下来开始搜索,经过一些测试后,可以从日志文件中加载最佳调度并应用。 + +``` python +# 运行自动调优(搜索) +# 注意:不在网页服务器中运行调优,因为它需要的时间太长。 +# 取消以下行的注释,来运行它。 +task.tune(tune_option, search_policy) + +# 应用最佳 schedule +sch, args = task.apply_best(log_file) +``` + +输出结果: + +``` bash +/usr/local/lib/python3.7/dist-packages/numpy/core/_methods.py:43: RuntimeWarning: invalid value encountered in reduce + return umr_minimum(a, axis, None, out, keepdims, initial, where) +/usr/local/lib/python3.7/dist-packages/numpy/core/_methods.py:39: RuntimeWarning: invalid value encountered in reduce + return umr_maximum(a, axis, None, out, keepdims, initial, where) +``` + +自动调度后对 schedule 降级,来查看 IR。auto-scheduler 正确执行优化,包括多级平铺、布局转换、并行化、向量化、展开和算子融合。 + +``` python +print("Lowered TIR:") +print(tvm.lower(sch, args, simple_mode=True)) +``` + +输出结果: + +``` bash +Lowered TIR: +@main = primfn(placeholder_5: handle, placeholder_6: handle, placeholder_7: handle, placeholder_8: handle, placeholder_9: handle, compute_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {placeholder: Buffer(placeholder_10: Pointer(float32), float32, [32768], []), + placeholder_1: Buffer(placeholder_11: Pointer(float32), float32, [78656], []), + placeholder_2: Buffer(placeholder_12: Pointer(int32), int32, [4916], []), + placeholder_3: Buffer(placeholder_13: Pointer(int32), int32, [33], []), + placeholder_4: Buffer(placeholder_14: Pointer(float32), float32, [65536], []), + compute: Buffer(compute_2: Pointer(float32), float32, [65536], [])} + buffer_map = {placeholder_5: placeholder, placeholder_6: placeholder_1, placeholder_7: placeholder_2, placeholder_8: placeholder_3, placeholder_9: placeholder_4, compute_1: compute} + preflattened_buffer_map = {placeholder_5: placeholder_15: Buffer(placeholder_10, float32, [128, 256], []), compute_1: compute_3: Buffer(compute_2, float32, [128, 512], []), placeholder_9: placeholder_16: Buffer(placeholder_14, float32, [128, 512], []), placeholder_7: placeholder_17: Buffer(placeholder_12, int32, [4916], []), placeholder_6: placeholder_18: Buffer(placeholder_11, float32, [4916, 16, 1], []), placeholder_8: placeholder_19: Buffer(placeholder_13, int32, [33], [])} { + for (i0.outer: int32, 0, 32) "parallel" { + allocate(compute_4: Pointer(global float32), float32, [64]), storage_scope = global; + for (i1.outer: int32, 0, 32) { + compute_5: Buffer(compute_4, float32, [64], [])[0] = 0f32 + compute_5[1] = 0f32 + compute_5[2] = 0f32 + compute_5[3] = 0f32 + compute_5[4] = 0f32 + compute_5[5] = 0f32 + compute_5[6] = 0f32 + compute_5[7] = 0f32 + compute_5[8] = 0f32 + compute_5[9] = 0f32 + compute_5[10] = 0f32 + compute_5[11] = 0f32 + compute_5[12] = 0f32 + compute_5[13] = 0f32 + compute_5[14] = 0f32 + compute_5[15] = 0f32 + compute_5[16] = 0f32 + compute_5[17] = 0f32 + compute_5[18] = 0f32 + compute_5[19] = 0f32 + compute_5[20] = 0f32 + compute_5[21] = 0f32 + compute_5[22] = 0f32 + compute_5[23] = 0f32 + compute_5[24] = 0f32 + compute_5[25] = 0f32 + compute_5[26] = 0f32 + compute_5[27] = 0f32 + compute_5[28] = 0f32 + compute_5[29] = 0f32 + compute_5[30] = 0f32 + compute_5[31] = 0f32 + compute_5[32] = 0f32 + compute_5[33] = 0f32 + compute_5[34] = 0f32 + compute_5[35] = 0f32 + compute_5[36] = 0f32 + compute_5[37] = 0f32 + compute_5[38] = 0f32 + compute_5[39] = 0f32 + compute_5[40] = 0f32 + compute_5[41] = 0f32 + compute_5[42] = 0f32 + compute_5[43] = 0f32 + compute_5[44] = 0f32 + compute_5[45] = 0f32 + compute_5[46] = 0f32 + compute_5[47] = 0f32 + compute_5[48] = 0f32 + compute_5[49] = 0f32 + compute_5[50] = 0f32 + compute_5[51] = 0f32 + compute_5[52] = 0f32 + compute_5[53] = 0f32 + compute_5[54] = 0f32 + compute_5[55] = 0f32 + compute_5[56] = 0f32 + compute_5[57] = 0f32 + compute_5[58] = 0f32 + compute_5[59] = 0f32 + compute_5[60] = 0f32 + compute_5[61] = 0f32 + compute_5[62] = 0f32 + compute_5[63] = 0f32 + for (elem_idx: int32, 0, (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])) { + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[0] = (compute_5[0] + (placeholder_1[((placeholder_3[i1.outer]*16) + (elem_idx*16))]*max(placeholder[((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)])], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[1] = (compute_5[1] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 1)]*max(placeholder[((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)])], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[2] = (compute_5[2] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 2)]*max(placeholder[((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)])], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[3] = (compute_5[3] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 3)]*max(placeholder[((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)])], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[4] = (compute_5[4] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 4)]*max(placeholder[((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)])], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[5] = (compute_5[5] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 5)]*max(placeholder[((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)])], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[6] = (compute_5[6] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 6)]*max(placeholder[((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)])], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[7] = (compute_5[7] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 7)]*max(placeholder[((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)])], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[8] = (compute_5[8] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 8)]*max(placeholder[((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)])], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[9] = (compute_5[9] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 9)]*max(placeholder[((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)])], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[10] = (compute_5[10] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 10)]*max(placeholder[((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)])], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[11] = (compute_5[11] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 11)]*max(placeholder[((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)])], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[12] = (compute_5[12] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 12)]*max(placeholder[((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)])], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[13] = (compute_5[13] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 13)]*max(placeholder[((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)])], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[14] = (compute_5[14] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 14)]*max(placeholder[((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)])], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[15] = (compute_5[15] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 15)]*max(placeholder[((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)])], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[16] = (compute_5[16] + (placeholder_1[((placeholder_3[i1.outer]*16) + (elem_idx*16))]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 256)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[17] = (compute_5[17] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 1)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 256)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[18] = (compute_5[18] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 2)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 256)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[19] = (compute_5[19] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 3)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 256)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[20] = (compute_5[20] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 4)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 256)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[21] = (compute_5[21] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 5)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 256)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[22] = (compute_5[22] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 6)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 256)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[23] = (compute_5[23] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 7)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 256)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[24] = (compute_5[24] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 8)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 256)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[25] = (compute_5[25] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 9)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 256)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[26] = (compute_5[26] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 10)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 256)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[27] = (compute_5[27] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 11)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 256)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[28] = (compute_5[28] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 12)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 256)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[29] = (compute_5[29] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 13)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 256)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[30] = (compute_5[30] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 14)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 256)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[31] = (compute_5[31] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 15)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 256)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[32] = (compute_5[32] + (placeholder_1[((placeholder_3[i1.outer]*16) + (elem_idx*16))]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 512)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[33] = (compute_5[33] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 1)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 512)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[34] = (compute_5[34] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 2)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 512)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[35] = (compute_5[35] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 3)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 512)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[36] = (compute_5[36] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 4)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 512)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[37] = (compute_5[37] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 5)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 512)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[38] = (compute_5[38] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 6)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 512)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[39] = (compute_5[39] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 7)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 512)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[40] = (compute_5[40] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 8)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 512)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[41] = (compute_5[41] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 9)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 512)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[42] = (compute_5[42] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 10)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 512)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[43] = (compute_5[43] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 11)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 512)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[44] = (compute_5[44] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 12)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 512)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[45] = (compute_5[45] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 13)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 512)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[46] = (compute_5[46] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 14)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 512)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[47] = (compute_5[47] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 15)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 512)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[48] = (compute_5[48] + (placeholder_1[((placeholder_3[i1.outer]*16) + (elem_idx*16))]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 768)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[49] = (compute_5[49] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 1)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 768)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[50] = (compute_5[50] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 2)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 768)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[51] = (compute_5[51] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 3)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 768)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[52] = (compute_5[52] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 4)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 768)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[53] = (compute_5[53] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 5)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 768)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[54] = (compute_5[54] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 6)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 768)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[55] = (compute_5[55] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 7)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 768)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[56] = (compute_5[56] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 8)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 768)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[57] = (compute_5[57] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 9)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 768)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[58] = (compute_5[58] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 10)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 768)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[59] = (compute_5[59] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 11)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 768)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[60] = (compute_5[60] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 12)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 768)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[61] = (compute_5[61] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 13)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 768)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[62] = (compute_5[62] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 14)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 768)], 0f32))) + } + if @tir.likely((elem_idx < (placeholder_3[(i1.outer + 1)] - placeholder_3[i1.outer])), dtype=bool) { + compute_5[63] = (compute_5[63] + (placeholder_1[(((placeholder_3[i1.outer]*16) + (elem_idx*16)) + 15)]*max(placeholder[(((i0.outer*1024) + placeholder_2[(placeholder_3[i1.outer] + elem_idx)]) + 768)], 0f32))) + } + } + for (i0.inner: int32, 0, 4) { + let cse_var_1: int32 = (((i0.outer*2048) + (i0.inner*512)) + (i1.outer*16)) + compute[ramp(cse_var_1, 1, 16)] = max((compute_5[ramp((i0.inner*16), 1, 16)] + placeholder_4[ramp(cse_var_1, 1, 16)]), broadcast(0f32, 16)) + } + } + } +} +``` + +## 检查正确性并评估性能 + +构建二进制文件,并检查其正确性和性能。 + +``` python +func = tvm.build(sch, args, target) + +dev = tvm.cpu() + +X_tvm = tvm.nd.array(X_np, device=dev) +W_data_tvm = tvm.nd.array(W_sp_np.data, device=dev) +W_indices_tvm = tvm.nd.array(W_sp_np.indices, device=dev) +W_indptr_tvm = tvm.nd.array(W_sp_np.indptr, device=dev) +B_tvm = tvm.nd.array(B_np, device=dev) +Y_tvm = tvm.nd.empty(Y_np.shape, device=dev) + +func(X_tvm, W_data_tvm, W_indices_tvm, W_indptr_tvm, B_tvm, Y_tvm) + +# 检查结果 +tvm.testing.assert_allclose(Y_np, Y_tvm.numpy(), atol=1e-4, rtol=1e-4) + +# 评估执行时间。 +evaluator = func.time_evaluator(func.entry_name, dev, min_repeat_ms=500) +print( + "Execution time of this operator: %.3f ms" + % ( + np.median(evaluator(X_tvm, W_data_tvm, W_indices_tvm, W_indptr_tvm, B_tvm, Y_tvm).results) + * 1000 + ) +) +``` + +输出结果: + +``` bash +Execution time of this operator: 2.616 ms +``` + +:::note +调优结果示例 + +``` bash +---------------------------------------------------------------------- +Lowered TIR: +primfn(placeholder_5: handle, placeholder_6: handle, placeholder_7: handle, placeholder_8: handle, placeholder_9: handle, compute_1: handle) -> () + attr = {"global_symbol": "main", "tir.noalias": True} + buffers = {placeholder_2: Buffer(placeholder_10: Pointer(float32), float32, [9831, 16, 1], []), + placeholder_4: Buffer(placeholder_11: Pointer(int32), int32, [33], []), + placeholder_3: Buffer(placeholder_12: Pointer(float32), float32, [512, 512], []), + compute: Buffer(compute_2: Pointer(float32), float32, [512, 512], []), + placeholder_1: Buffer(placeholder_13: Pointer(float32), float32, [512, 512], []), + placeholder: Buffer(placeholder_14: Pointer(int32), int32, [9831], [])} + buffer_map = {placeholder_7: placeholder, placeholder_9: placeholder_1, placeholder_6: placeholder_2, compute_1: compute, placeholder_5: placeholder_3, placeholder_8: placeholder_4} { + for (i0.outer.i1.outer.fused: int32, 0, 1024) "parallel" { + attr [compute_3: Pointer(float32)] "storage_scope" = "global"; + allocate(compute_3, float32, [256]) { + for (nb_j.inner: int32, 0, 2) { + for (i.inner.init: int32, 0, 8) { + for (j.init: int32, 0, 16) { + compute_3[(((i.inner.init*32) + (nb_j.inner*16)) + j.init)] = 0f32 + } + } + for (elem_idx: int32, 0, ((int32*)placeholder_11[(((floormod(i0.outer.i1.outer.fused, 16)*2) + nb_j.inner) + 1)] - (int32*)placeholder_11[((floormod(i0.outer.i1.outer.fused, 16)*2) + nb_j.inner)])) { + for (i.inner: int32, 0, 8) { + for (j: int32, 0, 16) { + compute_3[(((i.inner*32) + (nb_j.inner*16)) + j)] = ((float32*)compute_3[(((i.inner*32) + (nb_j.inner*16)) + j)] + ((float32*)placeholder_10[((((int32*)placeholder_11[((floormod(i0.outer.i1.outer.fused, 16)*2) + nb_j.inner)]*16) + (elem_idx*16)) + j)]*max((float32*)placeholder_12[(((floordiv(i0.outer.i1.outer.fused, 16)*4096) + (i.inner*512)) + (int32*)placeholder_14[((int32*)placeholder_11[((floormod(i0.outer.i1.outer.fused, 16)*2) + nb_j.inner)] + elem_idx)])], 0f32))) + } + } + } + } + for (i0.inner: int32, 0, 8) { + compute_2[ramp((((floordiv(i0.outer.i1.outer.fused, 16)*4096) + (i0.inner*512)) + (floormod(i0.outer.i1.outer.fused, 16)*32)), 1, 32)] = max(((float32x32*)compute_3[ramp((i0.inner*32), 1, 32)] + (float32x32*)placeholder_13[ramp((((floordiv(i0.outer.i1.outer.fused, 16)*4096) + (i0.inner*512)) + (floormod(i0.outer.i1.outer.fused, 16)*32)), 1, 32)]), broadcast(0f32, 32)) + } + } + } +} +``` + +[下载 Python 源代码:tune_sparse_x86.py](https://tvm.apache.org/docs/_downloads/07733b6b2cc4df026fce525285e8f538/tune_sparse_x86.py) + +[下载 Jupyter Notebook:tune_sparse_x86.ipynb](https://tvm.apache.org/docs/_downloads/293f8d0753933b706a0b588f909fe38a/tune_sparse_x86.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/autoscheduler/_category_.json b/versioned_docs/version-0.12.0/how_to/autoscheduler/_category_.json new file mode 100644 index 00000000..e71bd728 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/autoscheduler/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 170 +} diff --git a/versioned_docs/version-0.12.0/how_to/autoscheduler/index.md b/versioned_docs/version-0.12.0/how_to/autoscheduler/index.md new file mode 100644 index 00000000..e44340fd --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/autoscheduler/index.md @@ -0,0 +1,12 @@ +--- +title: 使用 AutoScheduler 进行无模板调度 +--- + +TVM AutoScheduler 提供了无需模板调优模型的方法。以下演示了如何针对常见平台对不同的模型进行调优。 + +* [为 GPU 自动调度卷积层](autoschedule_gpu) +* [为 x86 CPU 自动调度神经网络](autoschedule_x86) +* [为 NVIDIA GPU 自动调度神经网络](autoschedule_nvidia) +* [为 ARM CPU 自动调度神经网络](autoschedule_arm) +* [为 Mali GPU 自动调度神经网络](autoschedule_mali) +* [使用自定义调度规则(Sketch Rule)在 CPU 上自动调度稀疏矩阵乘法](autoschedule_custom) diff --git a/versioned_docs/version-0.12.0/how_to/autotune/01-tuning_nvidia.md b/versioned_docs/version-0.12.0/how_to/autotune/01-tuning_nvidia.md new file mode 100644 index 00000000..0981b202 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/autotune/01-tuning_nvidia.md @@ -0,0 +1,1884 @@ +--- +title: 在 NVIDIA GPU 上调优高性能卷积 +--- + +# 在 NVIDIA GPU 上调优高性能卷积 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/tune_with_autotvm/tune_conv2d_cuda.html#sphx-glr-download-how-to-tune-with-autotvm-tune-conv2d-cuda-py) 下载完整的示例代码 +::: + +**作者**:[Lianmin Zheng](https://github.com/merrymercy) + +本教程介绍如何为 NVIDIA GPU 编写高性能可调模板。通过在此模板上运行自动调优器,可在许多情况下胜过供应商提供的 cuDNN 库。 + +注意,本教程不会在 Windows 或最新版本的 macOS 上运行。如需运行,请将本教程的主体放在 `if __name__ == "__main__":` 代码块中。 + +## 安装依赖 + +要在 TVM 中使用 autotvm 包,需要安装额外的依赖(如果用的是 Python2,请将「3」更改为「2」): + +``` bash +pip3 install --user psutil xgboost tornado cloudpickle +``` + +为了让 TVM 在调优中运行更快,推荐使用 Cython 作为 TVM 的 FFI。在 TVM 的根目录下,执行如下命令: + +``` bash +pip3 install --user cython +sudo make cython3 +``` + +在 Python 代码中导入包: + +``` python +import logging +import sys +import numpy as np + +import tvm +from tvm import te, topi, testing +from tvm.topi.testing import conv2d_nchw_python +import tvm.testing + +from tvm import autotvm +``` + +## 第 1 步:定义搜索空间 + +TVM 中有很多有用的调度原语,详细教程,例如(1)[如何在 GPU 上优化卷积](/docs/how_to/optimize/gpu_conv)(2)[在 NVIDIA GPU 上优化 DepthwiseConv](https://tvm.apache.org/2017/08/22/Optimize-Deep-Learning-GPU-Operators-with-TVM-A-Depthwise-Convolution-Example)。 + +但是,它们的实现是针对一些特殊的输入 shape 手动调整的。本节将构建足够大的空间,涵盖这些教程中使用的技术,然后依靠高效的自动调优器,对空间进行搜索并选择合适的配置。 + +熟悉 CUDA schedule 的开发者,对以下通用模板并不陌生。可以修改此模板以调整其他算子,例如深度卷积和 GEMM。要完全理解这个模板,需要熟悉调度原语和自动调优 API,可以参考上面的教程和 [AutoTVM 教程](/docs/tutorial/ops_AutoTVM)。 + +需要注意 conv2d 算子的搜索空间可能非常大(某些输入 shape 为 10^9 级别) + +``` python +@autotvm.template("tutorial/conv2d_no_batching") +def conv2d_no_batching(N, H, W, CO, CI, KH, KW, stride, padding): + assert N == 1, "Only consider batch_size = 1 in this template" + + data = te.placeholder((N, CI, H, W), name="data") + kernel = te.placeholder((CO, CI, KH, KW), name="kernel") + conv = topi.nn.conv2d_nchw(data, kernel, stride, padding, dilation=1, out_dtype="float32") + s = te.create_schedule([conv.op]) + + #### 空间定义开始 #### + n, f, y, x = s[conv].op.axis + rc, ry, rx = s[conv].op.reduce_axis + + cfg = autotvm.get_config() + cfg.define_split("tile_f", f, num_outputs=4) + cfg.define_split("tile_y", y, num_outputs=4) + cfg.define_split("tile_x", x, num_outputs=4) + cfg.define_split("tile_rc", rc, num_outputs=3) + cfg.define_split("tile_ry", ry, num_outputs=3) + cfg.define_split("tile_rx", rx, num_outputs=3) + cfg.define_knob("auto_unroll_max_step", [0, 512, 1500]) + cfg.define_knob("unroll_explicit", [0, 1]) + #### 空间定义结束 #### + + # 内联填充 + pad_data = s[conv].op.input_tensors[0] + s[pad_data].compute_inline() + data, raw_data = pad_data, data + + output = conv + OL = s.cache_write(conv, "local") + + # 创建 cache 阶段 + AA = s.cache_read(data, "shared", [OL]) + WW = s.cache_read(kernel, "shared", [OL]) + AL = s.cache_read(AA, "local", [OL]) + WL = s.cache_read(WW, "local", [OL]) + + # 平铺和绑定空间轴 + n, f, y, x = s[output].op.axis + bf, vf, tf, fi = cfg["tile_f"].apply(s, output, f) + by, vy, ty, yi = cfg["tile_y"].apply(s, output, y) + bx, vx, tx, xi = cfg["tile_x"].apply(s, output, x) + kernel_scope = n # 这是在此内核中附加全局配置的范围 + + s[output].bind(bf, te.thread_axis("blockIdx.z")) + s[output].bind(by, te.thread_axis("blockIdx.y")) + s[output].bind(bx, te.thread_axis("blockIdx.x")) + s[output].bind(vf, te.thread_axis("vthread")) + s[output].bind(vy, te.thread_axis("vthread")) + s[output].bind(vx, te.thread_axis("vthread")) + s[output].bind(tf, te.thread_axis("threadIdx.z")) + s[output].bind(ty, te.thread_axis("threadIdx.y")) + s[output].bind(tx, te.thread_axis("threadIdx.x")) + s[output].reorder(n, bf, by, bx, vf, vy, vx, tf, ty, tx, fi, yi, xi) + s[OL].compute_at(s[output], tx) + + # tile reduction 轴 + n, f, y, x = s[OL].op.axis + rc, ry, rx = s[OL].op.reduce_axis + rco, rcm, rci = cfg["tile_rc"].apply(s, OL, rc) + ryo, rym, ryi = cfg["tile_rx"].apply(s, OL, ry) + rxo, rxm, rxi = cfg["tile_ry"].apply(s, OL, rx) + s[OL].reorder(rco, ryo, rxo, rcm, rym, rxm, rci, ryi, rxi, n, f, y, x) + + s[AA].compute_at(s[OL], rxo) + s[WW].compute_at(s[OL], rxo) + s[AL].compute_at(s[OL], rxm) + s[WL].compute_at(s[OL], rxm) + + # 协作获取 + for load in [AA, WW]: + n, f, y, x = s[load].op.axis + fused = s[load].fuse(n, f, y, x) + tz, fused = s[load].split(fused, nparts=cfg["tile_f"].size[2]) + ty, fused = s[load].split(fused, nparts=cfg["tile_y"].size[2]) + tx, fused = s[load].split(fused, nparts=cfg["tile_x"].size[2]) + s[load].bind(tz, te.thread_axis("threadIdx.z")) + s[load].bind(ty, te.thread_axis("threadIdx.y")) + s[load].bind(tx, te.thread_axis("threadIdx.x")) + + # 调优 unroll + s[output].pragma(kernel_scope, "auto_unroll_max_step", cfg["auto_unroll_max_step"].val) + s[output].pragma(kernel_scope, "unroll_explicit", cfg["unroll_explicit"].val) + + return s, [raw_data, kernel, conv] +``` + +## 第 2 步:在空间中搜索 + +选择 resnet 上的最后一层作为测试用例。由于空间足够大,所以 `XGBoostTuner` 最适合。这里只做 20 次试验来演示。实际上试验 1000 次可以为这个模板找到更合适的内核。 + +``` python +# logging 配置(用于将调优日志打印到屏幕) +logging.getLogger("autotvm").setLevel(logging.DEBUG) +logging.getLogger("autotvm").addHandler(logging.StreamHandler(sys.stdout)) + +# resnet 中的最后一层 +N, H, W, CO, CI, KH, KW, strides, padding = 1, 7, 7, 512, 512, 3, 3, (1, 1), (1, 1) +task = autotvm.task.create( + "tutorial/conv2d_no_batching", args=(N, H, W, CO, CI, KH, KW, strides, padding), target="cuda" +) +print(task.config_space) + +# 使用本地 gpu,为每个配置测量 10 次以减少方差 +# 编译程序超时 10 秒,运行超时 4 秒 +measure_option = autotvm.measure_option( + builder=autotvm.LocalBuilder(), + runner=autotvm.LocalRunner(repeat=3, min_repeat_ms=100, timeout=4), +) + +# 开始调优,将 log 记录到 `conv2d.log` +# 在调优过程中,会尝试很多无效的配置,所以你应该 +# 查看许多错误报告。只要能看到非零的 GFLOPS 就可以。 +tuner = autotvm.tuner.XGBTuner(task) +tuner.tune( + n_trial=20, + measure_option=measure_option, + callbacks=[autotvm.callback.log_to_file("conv2d.log")], +) +``` + +输出结果: + +``` bash +ConfigSpace (len=10454400, space_map= + 0 tile_f: Split(policy=factors, product=512, num_outputs=4) len=220 + 1 tile_y: Split(policy=factors, product=7, num_outputs=4) len=4 + 2 tile_x: Split(policy=factors, product=7, num_outputs=4) len=4 + 3 tile_rc: Split(policy=factors, product=512, num_outputs=3) len=55 + 4 tile_ry: Split(policy=factors, product=3, num_outputs=3) len=3 + 5 tile_rx: Split(policy=factors, product=3, num_outputs=3) len=3 + 6 auto_unroll_max_step: OtherOption([0, 512, 1500]) len=3 + 7 unroll_explicit: OtherOption([0, 1]) len=2 +) +waiting for device... +device available +Get devices for measurement successfully! +No: 1 GFLOPS: 0.00/0.00 result: Traceback (most recent call last): + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 588, in __call__ + func, arg_info = _build_func_common(measure_input, self.runtime, **kwargs) + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 540, in _build_func_common + func = build(s, args, target_host=task.target_host, runtime=runtime) + File "/workspace/python/tvm/driver/build_module.py", line 228, in build + input_mod = lower(inputs, args, name=name, binds=binds) + File "/workspace/python/tvm/driver/build_module.py", line 134, in lower + return ffi.lower_schedule(inp, args, name, binds, simple_mode) + File "tvm/_ffi/_cython/./packed_func.pxi", line 331, in tvm._ffi._cy3.core.PackedFuncBase.__call__ + File "tvm/_ffi/_cython/./packed_func.pxi", line 276, in tvm._ffi._cy3.core.FuncCall + File "tvm/_ffi/_cython/./base.pxi", line 181, in tvm._ffi._cy3.core.CHECK_CALL +tvm._ffi.base.TVMError: Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel + +Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel [('tile_f', [-1, 32, 8, 2]), ('tile_y', [-1, 1, 1, 1]), ('tile_x', [-1, 7, 1, 1]), ('tile_rc', [-1, 8, 64]), ('tile_ry', [-1, 3, 1]), ('tile_rx', [-1, 3, 1]), ('auto_unroll_max_step', 0), ('unroll_explicit', 1)],None,6171524 +No: 2 GFLOPS: 0.00/0.00 result: Traceback (most recent call last): + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 588, in __call__ + func, arg_info = _build_func_common(measure_input, self.runtime, **kwargs) + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 540, in _build_func_common + func = build(s, args, target_host=task.target_host, runtime=runtime) + File "/workspace/python/tvm/driver/build_module.py", line 228, in build + input_mod = lower(inputs, args, name=name, binds=binds) + File "/workspace/python/tvm/driver/build_module.py", line 134, in lower + return ffi.lower_schedule(inp, args, name, binds, simple_mode) + File "tvm/_ffi/_cython/./packed_func.pxi", line 331, in tvm._ffi._cy3.core.PackedFuncBase.__call__ + File "tvm/_ffi/_cython/./packed_func.pxi", line 276, in tvm._ffi._cy3.core.FuncCall + File "tvm/_ffi/_cython/./base.pxi", line 181, in tvm._ffi._cy3.core.CHECK_CALL +tvm._ffi.base.TVMError: Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel + +Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel [('tile_f', [-1, 128, 1, 4]), ('tile_y', [-1, 1, 1, 1]), ('tile_x', [-1, 1, 1, 1]), ('tile_rc', [-1, 4, 128]), ('tile_ry', [-1, 1, 1]), ('tile_rx', [-1, 3, 1]), ('auto_unroll_max_step', 512), ('unroll_explicit', 0)],None,2502827 +No: 3 GFLOPS: 0.00/0.00 result: Traceback (most recent call last): + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 588, in __call__ + func, arg_info = _build_func_common(measure_input, self.runtime, **kwargs) + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 540, in _build_func_common + func = build(s, args, target_host=task.target_host, runtime=runtime) + File "/workspace/python/tvm/driver/build_module.py", line 228, in build + input_mod = lower(inputs, args, name=name, binds=binds) + File "/workspace/python/tvm/driver/build_module.py", line 134, in lower + return ffi.lower_schedule(inp, args, name, binds, simple_mode) + File "tvm/_ffi/_cython/./packed_func.pxi", line 331, in tvm._ffi._cy3.core.PackedFuncBase.__call__ + File "tvm/_ffi/_cython/./packed_func.pxi", line 276, in tvm._ffi._cy3.core.FuncCall + File "tvm/_ffi/_cython/./base.pxi", line 181, in tvm._ffi._cy3.core.CHECK_CALL +tvm._ffi.base.TVMError: Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel + +Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel [('tile_f', [-1, 4, 1, 32]), ('tile_y', [-1, 1, 1, 1]), ('tile_x', [-1, 1, 1, 7]), ('tile_rc', [-1, 512, 1]), ('tile_ry', [-1, 1, 3]), ('tile_rx', [-1, 1, 3]), ('auto_unroll_max_step', 512), ('unroll_explicit', 0)],None,3325707 +No: 4 GFLOPS: 0.00/0.00 result: Traceback (most recent call last): + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 588, in __call__ + func, arg_info = _build_func_common(measure_input, self.runtime, **kwargs) + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 540, in _build_func_common + func = build(s, args, target_host=task.target_host, runtime=runtime) + File "/workspace/python/tvm/driver/build_module.py", line 228, in build + input_mod = lower(inputs, args, name=name, binds=binds) + File "/workspace/python/tvm/driver/build_module.py", line 134, in lower + return ffi.lower_schedule(inp, args, name, binds, simple_mode) + File "tvm/_ffi/_cython/./packed_func.pxi", line 331, in tvm._ffi._cy3.core.PackedFuncBase.__call__ + File "tvm/_ffi/_cython/./packed_func.pxi", line 276, in tvm._ffi._cy3.core.FuncCall + File "tvm/_ffi/_cython/./base.pxi", line 181, in tvm._ffi._cy3.core.CHECK_CALL +tvm._ffi.base.TVMError: Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel + +Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel [('tile_f', [-1, 8, 4, 2]), ('tile_y', [-1, 1, 1, 7]), ('tile_x', [-1, 1, 1, 1]), ('tile_rc', [-1, 4, 8]), ('tile_ry', [-1, 3, 1]), ('tile_rx', [-1, 1, 3]), ('auto_unroll_max_step', 1500), ('unroll_explicit', 0)],None,4942815 +No: 5 GFLOPS: 0.00/0.00 result: Traceback (most recent call last): + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 588, in __call__ + func, arg_info = _build_func_common(measure_input, self.runtime, **kwargs) + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 540, in _build_func_common + func = build(s, args, target_host=task.target_host, runtime=runtime) + File "/workspace/python/tvm/driver/build_module.py", line 228, in build + input_mod = lower(inputs, args, name=name, binds=binds) + File "/workspace/python/tvm/driver/build_module.py", line 134, in lower + return ffi.lower_schedule(inp, args, name, binds, simple_mode) + File "tvm/_ffi/_cython/./packed_func.pxi", line 331, in tvm._ffi._cy3.core.PackedFuncBase.__call__ + File "tvm/_ffi/_cython/./packed_func.pxi", line 276, in tvm._ffi._cy3.core.FuncCall + File "tvm/_ffi/_cython/./base.pxi", line 181, in tvm._ffi._cy3.core.CHECK_CALL +tvm._ffi.base.TVMError: Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel + +Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel [('tile_f', [-1, 4, 1, 128]), ('tile_y', [-1, 1, 1, 7]), ('tile_x', [-1, 7, 1, 1]), ('tile_rc', [-1, 2, 64]), ('tile_ry', [-1, 1, 3]), ('tile_rx', [-1, 1, 3]), ('auto_unroll_max_step', 1500), ('unroll_explicit', 0)],None,5197272 +No: 6 GFLOPS: 0.00/0.00 result: Traceback (most recent call last): + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 588, in __call__ + func, arg_info = _build_func_common(measure_input, self.runtime, **kwargs) + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 540, in _build_func_common + func = build(s, args, target_host=task.target_host, runtime=runtime) + File "/workspace/python/tvm/driver/build_module.py", line 228, in build + input_mod = lower(inputs, args, name=name, binds=binds) + File "/workspace/python/tvm/driver/build_module.py", line 134, in lower + return ffi.lower_schedule(inp, args, name, binds, simple_mode) + File "tvm/_ffi/_cython/./packed_func.pxi", line 331, in tvm._ffi._cy3.core.PackedFuncBase.__call__ + File "tvm/_ffi/_cython/./packed_func.pxi", line 276, in tvm._ffi._cy3.core.FuncCall + File "tvm/_ffi/_cython/./base.pxi", line 181, in tvm._ffi._cy3.core.CHECK_CALL +tvm._ffi.base.TVMError: Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel + +Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel [('tile_f', [-1, 32, 2, 4]), ('tile_y', [-1, 1, 1, 1]), ('tile_x', [-1, 1, 7, 1]), ('tile_rc', [-1, 8, 8]), ('tile_ry', [-1, 1, 3]), ('tile_rx', [-1, 1, 1]), ('auto_unroll_max_step', 1500), ('unroll_explicit', 0)],None,3979473 +No: 7 GFLOPS: 0.00/0.00 result: Traceback (most recent call last): + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 588, in __call__ + func, arg_info = _build_func_common(measure_input, self.runtime, **kwargs) + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 540, in _build_func_common + func = build(s, args, target_host=task.target_host, runtime=runtime) + File "/workspace/python/tvm/driver/build_module.py", line 228, in build + input_mod = lower(inputs, args, name=name, binds=binds) + File "/workspace/python/tvm/driver/build_module.py", line 134, in lower + return ffi.lower_schedule(inp, args, name, binds, simple_mode) + File "tvm/_ffi/_cython/./packed_func.pxi", line 331, in tvm._ffi._cy3.core.PackedFuncBase.__call__ + File "tvm/_ffi/_cython/./packed_func.pxi", line 276, in tvm._ffi._cy3.core.FuncCall + File "tvm/_ffi/_cython/./base.pxi", line 181, in tvm._ffi._cy3.core.CHECK_CALL +tvm._ffi.base.TVMError: Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel + +Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel [('tile_f', [-1, 8, 4, 8]), ('tile_y', [-1, 1, 7, 1]), ('tile_x', [-1, 1, 1, 1]), ('tile_rc', [-1, 4, 32]), ('tile_ry', [-1, 1, 3]), ('tile_rx', [-1, 1, 3]), ('auto_unroll_max_step', 512), ('unroll_explicit', 0)],None,3439632 +No: 8 GFLOPS: 0.00/0.00 result: Traceback (most recent call last): + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 142, in build + res = future.result() + File "/usr/lib/python3.7/concurrent/futures/_base.py", line 435, in result + return self.__get_result() + File "/usr/lib/python3.7/concurrent/futures/_base.py", line 384, in __get_result + raise self._exception + File "/usr/lib/python3.7/concurrent/futures/thread.py", line 57, in run + result = self.fn(*self.args, **self.kwargs) + File "/workspace/python/tvm/contrib/popen_pool.py", line 404, in + worker = lambda *args: self._worker_run(*args) + File "/workspace/python/tvm/contrib/popen_pool.py", line 373, in _worker_run + return proc.recv() + File "/workspace/python/tvm/contrib/popen_pool.py", line 297, in recv + raise TimeoutError() +TimeoutError + + [('tile_f', [-1, 2, 1, 64]), ('tile_y', [-1, 1, 1, 7]), ('tile_x', [-1, 1, 7, 1]), ('tile_rc', [-1, 1, 4]), ('tile_ry', [-1, 3, 1]), ('tile_rx', [-1, 1, 3]), ('auto_unroll_max_step', 1500), ('unroll_explicit', 0)],None,4909501 +No: 9 GFLOPS: 174.65/174.65 result: MeasureResult(costs=(0.0013254985555555556,), error_no=MeasureErrorNo.NO_ERROR, all_cost=2.0453152656555176, timestamp=1658801307.741073) [('tile_f', [-1, 1, 4, 8]), ('tile_y', [-1, 7, 1, 1]), ('tile_x', [-1, 1, 1, 1]), ('tile_rc', [-1, 2, 2]), ('tile_ry', [-1, 1, 3]), ('tile_rx', [-1, 1, 3]), ('auto_unroll_max_step', 1500), ('unroll_explicit', 0)],None,5072689 +No: 10 GFLOPS: 0.00/174.65 result: Traceback (most recent call last): + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 588, in __call__ + func, arg_info = _build_func_common(measure_input, self.runtime, **kwargs) + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 540, in _build_func_common + func = build(s, args, target_host=task.target_host, runtime=runtime) + File "/workspace/python/tvm/driver/build_module.py", line 228, in build + input_mod = lower(inputs, args, name=name, binds=binds) + File "/workspace/python/tvm/driver/build_module.py", line 134, in lower + return ffi.lower_schedule(inp, args, name, binds, simple_mode) + File "tvm/_ffi/_cython/./packed_func.pxi", line 331, in tvm._ffi._cy3.core.PackedFuncBase.__call__ + File "tvm/_ffi/_cython/./packed_func.pxi", line 276, in tvm._ffi._cy3.core.FuncCall + File "tvm/_ffi/_cython/./base.pxi", line 181, in tvm._ffi._cy3.core.CHECK_CALL +tvm._ffi.base.TVMError: Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel + +Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel [('tile_f', [-1, 4, 4, 8]), ('tile_y', [-1, 1, 1, 1]), ('tile_x', [-1, 1, 1, 7]), ('tile_rc', [-1, 64, 2]), ('tile_ry', [-1, 1, 3]), ('tile_rx', [-1, 1, 3]), ('auto_unroll_max_step', 1500), ('unroll_explicit', 0)],None,5092711 +No: 11 GFLOPS: 260.18/260.18 result: MeasureResult(costs=(0.0008897650828729283,), error_no=MeasureErrorNo.NO_ERROR, all_cost=1.7439014911651611, timestamp=1658801308.6655772) [('tile_f', [-1, 8, 2, 1]), ('tile_y', [-1, 7, 1, 1]), ('tile_x', [-1, 1, 7, 1]), ('tile_rc', [-1, 2, 1]), ('tile_ry', [-1, 3, 1]), ('tile_rx', [-1, 3, 1]), ('auto_unroll_max_step', 1500), ('unroll_explicit', 0)],None,4264713 +No: 12 GFLOPS: 0.00/260.18 result: Traceback (most recent call last): + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 588, in __call__ + func, arg_info = _build_func_common(measure_input, self.runtime, **kwargs) + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 540, in _build_func_common + func = build(s, args, target_host=task.target_host, runtime=runtime) + File "/workspace/python/tvm/driver/build_module.py", line 228, in build + input_mod = lower(inputs, args, name=name, binds=binds) + File "/workspace/python/tvm/driver/build_module.py", line 134, in lower + return ffi.lower_schedule(inp, args, name, binds, simple_mode) + File "tvm/_ffi/_cython/./packed_func.pxi", line 331, in tvm._ffi._cy3.core.PackedFuncBase.__call__ + File "tvm/_ffi/_cython/./packed_func.pxi", line 276, in tvm._ffi._cy3.core.FuncCall + File "tvm/_ffi/_cython/./base.pxi", line 181, in tvm._ffi._cy3.core.CHECK_CALL +tvm._ffi.base.TVMError: Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel + +Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel [('tile_f', [-1, 128, 1, 2]), ('tile_y', [-1, 1, 7, 1]), ('tile_x', [-1, 1, 1, 1]), ('tile_rc', [-1, 1, 256]), ('tile_ry', [-1, 1, 1]), ('tile_rx', [-1, 1, 1]), ('auto_unroll_max_step', 0), ('unroll_explicit', 0)],None,183542 +No: 13 GFLOPS: 0.00/260.18 result: Traceback (most recent call last): + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 588, in __call__ + func, arg_info = _build_func_common(measure_input, self.runtime, **kwargs) + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 540, in _build_func_common + func = build(s, args, target_host=task.target_host, runtime=runtime) + File "/workspace/python/tvm/driver/build_module.py", line 228, in build + input_mod = lower(inputs, args, name=name, binds=binds) + File "/workspace/python/tvm/driver/build_module.py", line 134, in lower + return ffi.lower_schedule(inp, args, name, binds, simple_mode) + File "tvm/_ffi/_cython/./packed_func.pxi", line 331, in tvm._ffi._cy3.core.PackedFuncBase.__call__ + File "tvm/_ffi/_cython/./packed_func.pxi", line 276, in tvm._ffi._cy3.core.FuncCall + File "tvm/_ffi/_cython/./base.pxi", line 181, in tvm._ffi._cy3.core.CHECK_CALL +tvm._ffi.base.TVMError: Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel + +Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel [('tile_f', [-1, 4, 8, 8]), ('tile_y', [-1, 1, 7, 1]), ('tile_x', [-1, 1, 1, 1]), ('tile_rc', [-1, 1, 64]), ('tile_ry', [-1, 1, 1]), ('tile_rx', [-1, 3, 1]), ('auto_unroll_max_step', 512), ('unroll_explicit', 0)],None,2482196 +No: 14 GFLOPS: 0.00/260.18 result: Traceback (most recent call last): + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 588, in __call__ + func, arg_info = _build_func_common(measure_input, self.runtime, **kwargs) + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 540, in _build_func_common + func = build(s, args, target_host=task.target_host, runtime=runtime) + File "/workspace/python/tvm/driver/build_module.py", line 228, in build + input_mod = lower(inputs, args, name=name, binds=binds) + File "/workspace/python/tvm/driver/build_module.py", line 134, in lower + return ffi.lower_schedule(inp, args, name, binds, simple_mode) + File "tvm/_ffi/_cython/./packed_func.pxi", line 331, in tvm._ffi._cy3.core.PackedFuncBase.__call__ + File "tvm/_ffi/_cython/./packed_func.pxi", line 276, in tvm._ffi._cy3.core.FuncCall + File "tvm/_ffi/_cython/./base.pxi", line 181, in tvm._ffi._cy3.core.CHECK_CALL +tvm._ffi.base.TVMError: Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel + +Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel [('tile_f', [-1, 64, 1, 4]), ('tile_y', [-1, 1, 7, 1]), ('tile_x', [-1, 1, 1, 7]), ('tile_rc', [-1, 4, 2]), ('tile_ry', [-1, 1, 3]), ('tile_rx', [-1, 1, 3]), ('auto_unroll_max_step', 1500), ('unroll_explicit', 1)],None,10306226 +No: 15 GFLOPS: 5.42/260.18 result: MeasureResult(costs=(0.042678113000000004,), error_no=MeasureErrorNo.NO_ERROR, all_cost=1.8313236236572266, timestamp=1658801313.2114427) [('tile_f', [-1, 2, 2, 8]), ('tile_y', [-1, 1, 1, 7]), ('tile_x', [-1, 7, 1, 1]), ('tile_rc', [-1, 4, 8]), ('tile_ry', [-1, 1, 1]), ('tile_rx', [-1, 1, 1]), ('auto_unroll_max_step', 0), ('unroll_explicit', 1)],None,5330964 +No: 16 GFLOPS: 3.34/260.18 result: MeasureResult(costs=(0.0693738225,), error_no=MeasureErrorNo.NO_ERROR, all_cost=4.546748638153076, timestamp=1658801314.4557946) [('tile_f', [-1, 8, 4, 4]), ('tile_y', [-1, 1, 1, 7]), ('tile_x', [-1, 1, 1, 7]), ('tile_rc', [-1, 4, 1]), ('tile_ry', [-1, 1, 3]), ('tile_rx', [-1, 1, 1]), ('auto_unroll_max_step', 512), ('unroll_explicit', 0)],None,2140058 +No: 17 GFLOPS: 0.00/260.18 result: Traceback (most recent call last): + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 142, in build + res = future.result() + File "/usr/lib/python3.7/concurrent/futures/_base.py", line 435, in result + return self.__get_result() + File "/usr/lib/python3.7/concurrent/futures/_base.py", line 384, in __get_result + raise self._exception + File "/usr/lib/python3.7/concurrent/futures/thread.py", line 57, in run + result = self.fn(*self.args, **self.kwargs) + File "/workspace/python/tvm/contrib/popen_pool.py", line 404, in + worker = lambda *args: self._worker_run(*args) + File "/workspace/python/tvm/contrib/popen_pool.py", line 373, in _worker_run + return proc.recv() + File "/workspace/python/tvm/contrib/popen_pool.py", line 297, in recv + raise TimeoutError() +TimeoutError + + [('tile_f', [-1, 2, 2, 1]), ('tile_y', [-1, 1, 7, 1]), ('tile_x', [-1, 7, 1, 1]), ('tile_rc', [-1, 4, 16]), ('tile_ry', [-1, 3, 1]), ('tile_rx', [-1, 1, 3]), ('auto_unroll_max_step', 1500), ('unroll_explicit', 1)],None,10195251 +No: 18 GFLOPS: 27.80/260.18 result: MeasureResult(costs=(0.008326810928571429,), error_no=MeasureErrorNo.NO_ERROR, all_cost=1.2733991146087646, timestamp=1658801325.4546382) [('tile_f', [-1, 4, 8, 4]), ('tile_y', [-1, 1, 1, 1]), ('tile_x', [-1, 1, 1, 1]), ('tile_rc', [-1, 1, 4]), ('tile_ry', [-1, 3, 1]), ('tile_rx', [-1, 3, 1]), ('auto_unroll_max_step', 0), ('unroll_explicit', 1)],None,6068603 +No: 19 GFLOPS: 0.00/260.18 result: Traceback (most recent call last): + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 588, in __call__ + func, arg_info = _build_func_common(measure_input, self.runtime, **kwargs) + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 540, in _build_func_common + func = build(s, args, target_host=task.target_host, runtime=runtime) + File "/workspace/python/tvm/driver/build_module.py", line 228, in build + input_mod = lower(inputs, args, name=name, binds=binds) + File "/workspace/python/tvm/driver/build_module.py", line 134, in lower + return ffi.lower_schedule(inp, args, name, binds, simple_mode) + File "tvm/_ffi/_cython/./packed_func.pxi", line 331, in tvm._ffi._cy3.core.PackedFuncBase.__call__ + File "tvm/_ffi/_cython/./packed_func.pxi", line 276, in tvm._ffi._cy3.core.FuncCall + File "tvm/_ffi/_cython/./base.pxi", line 181, in tvm._ffi._cy3.core.CHECK_CALL +tvm._ffi.base.TVMError: Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel + +Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel [('tile_f', [-1, 16, 4, 8]), ('tile_y', [-1, 1, 7, 1]), ('tile_x', [-1, 7, 1, 1]), ('tile_rc', [-1, 4, 128]), ('tile_ry', [-1, 1, 3]), ('tile_rx', [-1, 1, 3]), ('auto_unroll_max_step', 0), ('unroll_explicit', 1)],None,6956993 +No: 20 GFLOPS: 0.00/260.18 result: Traceback (most recent call last): + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 588, in __call__ + func, arg_info = _build_func_common(measure_input, self.runtime, **kwargs) + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 540, in _build_func_common + func = build(s, args, target_host=task.target_host, runtime=runtime) + File "/workspace/python/tvm/driver/build_module.py", line 228, in build + input_mod = lower(inputs, args, name=name, binds=binds) + File "/workspace/python/tvm/driver/build_module.py", line 134, in lower + return ffi.lower_schedule(inp, args, name, binds, simple_mode) + File "tvm/_ffi/_cython/./packed_func.pxi", line 331, in tvm._ffi._cy3.core.PackedFuncBase.__call__ + File "tvm/_ffi/_cython/./packed_func.pxi", line 276, in tvm._ffi._cy3.core.FuncCall + File "tvm/_ffi/_cython/./base.pxi", line 181, in tvm._ffi._cy3.core.CHECK_CALL +tvm._ffi.base.TVMError: Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel + +Traceback (most recent call last): + 24: TVMFuncCall + at ../src/runtime/c_runtime_api.cc:477 + 23: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 22: Call + at ../include/tvm/runtime/packed_func.h:1213 + 21: operator() + at ../include/tvm/runtime/packed_func.h:1731 + 20: unpack_call&, const tvm::runtime::String&, const tvm::runtime::Map&, bool)> > + at ../include/tvm/runtime/packed_func.h:1671 + 19: run<> + at ../include/tvm/runtime/packed_func.h:1631 + 18: run + at ../include/tvm/runtime/packed_func.h:1631 + 17: run + at ../include/tvm/runtime/packed_func.h:1631 + 16: run + at ../include/tvm/runtime/packed_func.h:1631 + 15: run + at ../include/tvm/runtime/packed_func.h:1631 + 14: run + at ../include/tvm/runtime/packed_func.h:1646 + 13: operator() + at ../src/driver/driver_api.cc:365 + 12: tvm::LowerSchedule(tvm::te::Schedule, tvm::runtime::Array const&, std::__cxx11::basic_string, std::allocator > const&, std::unordered_map, std::equal_to, std::allocator > > const&, bool) + at ../src/driver/driver_api.cc:352 + 11: tvm::LowerWithPassList(tvm::IRModule, tvm::runtime::Array) + at ../src/driver/driver_api.cc:252 + 10: tvm::transform::Pass::operator()(tvm::IRModule) const + at ../src/ir/transform.cc:258 + 9: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 8: tvm::transform::SequentialNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:453 + 7: tvm::transform::Pass::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/ir/transform.cc:274 + 6: tvm::tir::transform::PrimFuncPassNode::operator()(tvm::IRModule, tvm::transform::PassContext const&) const + at ../src/tir/ir/transform.cc:100 + 5: tvm::runtime::TypedPackedFunc::operator()(tvm::tir::PrimFunc, tvm::IRModule, tvm::transform::PassContext) const + at ../include/tvm/runtime/packed_func.h:1750 + 4: tvm::tir::PrimFunc tvm::runtime::detail::typed_packed_call_dispatcher::run(tvm::runtime::PackedFunc const&, tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) + at ../include/tvm/runtime/packed_func.h:1694 + 3: tvm::runtime::TVMRetValue tvm::runtime::PackedFunc::operator()(tvm::tir::PrimFunc&&, tvm::IRModule&&, tvm::transform::PassContext&&) const + at ../include/tvm/runtime/packed_func.h:1618 + 2: tvm::runtime::PackedFuncObj::CallPacked(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*) const + at ../include/tvm/runtime/packed_func.h:1217 + 1: Call + at ../include/tvm/runtime/packed_func.h:1213 + 0: operator() + at ../src/runtime/c_runtime_api.cc:534 + File "tvm/_ffi/_cython/./packed_func.pxi", line 56, in tvm._ffi._cy3.core.tvm_callback + File "/workspace/python/tvm/autotvm/measure/measure_methods.py", line 871, in verify_pass + raise InstantiationError("Skipped because of invalid gpu kernel") +tvm.autotvm.task.space.InstantiationError: Skipped because of invalid gpu kernel [('tile_f', [-1, 16, 1, 2]), ('tile_y', [-1, 7, 1, 1]), ('tile_x', [-1, 1, 7, 1]), ('tile_rc', [-1, 32, 4]), ('tile_ry', [-1, 1, 3]), ('tile_rx', [-1, 1, 3]), ('auto_unroll_max_step', 512), ('unroll_explicit', 0)],None,3377719 +``` + +最后从日志文件中检查最佳配置,检查正确性并测试运行时间。 + +``` python +# 检查最佳配置 +dispatch_context = autotvm.apply_history_best("conv2d.log") +best_config = dispatch_context.query(task.target, task.workload) +print("\nBest config:") +print(best_config) + +# 从日志文件中应用历史最好记录 +with autotvm.apply_history_best("conv2d.log"): + with tvm.target.Target("cuda"): + s, arg_bufs = conv2d_no_batching(N, H, W, CO, CI, KH, KW, strides, padding) + func = tvm.build(s, arg_bufs) + +# 验证正确性 +a_np = np.random.uniform(size=(N, CI, H, W)).astype(np.float32) +w_np = np.random.uniform(size=(CO, CI, KH, KW)).astype(np.float32) +c_np = conv2d_nchw_python(a_np, w_np, strides, padding) + +dev = tvm.cuda() +a_tvm = tvm.nd.array(a_np, device=dev) +w_tvm = tvm.nd.array(w_np, device=dev) +c_tvm = tvm.nd.empty(c_np.shape, device=dev) +func(a_tvm, w_tvm, c_tvm) + +tvm.testing.assert_allclose(c_np, c_tvm.numpy(), rtol=1e-2) + +# 评估运行时间。这里选择一个较大的重复次数(400)来减少噪音以及内核启动的开销。还可用 nvprof 来验证结果。 +evaluator = func.time_evaluator(func.entry_name, dev, number=400) +print("Time cost of this operator: %f" % evaluator(a_tvm, w_tvm, c_tvm).mean) +``` + +输出结果: + +``` bash +Finish loading 20 records + +Best config: +[('tile_f', [-1, 8, 2, 1]), ('tile_y', [-1, 7, 1, 1]), ('tile_x', [-1, 1, 7, 1]), ('tile_rc', [-1, 2, 1]), ('tile_ry', [-1, 3, 1]), ('tile_rx', [-1, 3, 1]), ('auto_unroll_max_step', 1500), ('unroll_explicit', 0)],None,4264713 +Finish loading 20 records +Time cost of this operator: 0.001299 +``` + +[下载 Python 源代码:tune_conv2d_cuda.py](https://tvm.apache.org/docs/_downloads/6ad550da5092845382b1197f58a93816/tune_conv2d_cuda.py) + +[下载 Jupyter notebook:tune_conv2d_cuda.ipynb](https://tvm.apache.org/docs/_downloads/732ed130cbc15432e737da8cc47e1734/tune_conv2d_cuda.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/autotune/02-autotuning_nvidia.md b/versioned_docs/version-0.12.0/how_to/autotune/02-autotuning_nvidia.md new file mode 100644 index 00000000..8bb312f0 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/autotune/02-autotuning_nvidia.md @@ -0,0 +1,342 @@ +--- +title: 为 NVIDIA GPU 自动调优卷积网络 +--- + +# 为 NVIDIA GPU 自动调优卷积网络 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/tune_with_autotvm/tune_relay_cuda.html#sphx-glr-download-how-to-tune-with-autotvm-tune-relay-cuda-py) 下载完整的示例代码 +::: + +**作者**:[Lianmin Zheng](https://github.com/merrymercy),[Eddie Yan](https://github.com/eqy/) + +针对特定设备和工作负载的自动调优对于获得最佳性能至关重要,本文介绍如何为 NVIDIA GPU 调优整个卷积网络。 + +TVM 中 NVIDIA GPU 的算子实现是以 template 形式编写的,该 template 有许多可调参数(tile 因子,unrolling 等)。对神经网络中的所有卷积和深度卷积算子调优后,会生成一个日志文件,它存储所有必需算子的最佳参数值。当 TVM 编译器编译这些算子时,会查询这个日志文件,从而获取最佳参数值。 + +我们还发布了一些 NVIDIA GPU 的预调参数,可以前往 [NVIDIA GPU Benchmark](https://github.com/apache/tvm/wiki/Benchmark#nvidia-gpu) 查看详细信息。 + +注意,本教程无法在 Windows 或最新版本的 macOS 上运行。如需运行,请将本教程的主体放在 `if __name__ == "__main__":` 代码块中。 + +## 安装依赖 + +要在 TVM 中使用 autotvm 包,需要安装额外的依赖(如果用的是 Python2,请将「3」更改为「2」): + +``` bash +pip3 install --user psutil xgboost tornado cloudpickle +``` + +为了让 TVM 在调优过程中运行更快,推荐使用 Cython 作为 TVM 的 FFI。在 TVM 的根目录下,执行如下命令: + +``` bash +pip3 install --user cython +sudo make cython3 +``` + +在 Python 代码中导入包: + +``` python +import os +import numpy as np + +import tvm +from tvm import relay, autotvm +import tvm.relay.testing +from tvm.autotvm.tuner import XGBTuner, GATuner, RandomTuner, GridSearchTuner +import tvm.contrib.graph_executor as runtime +``` + +## 定义网络 + +首先要在 Relay 前端 API 中定义网络,可以从 `tvm.relay.testing` 加载一些预定义的网络。也可以从 MXNet、ONNX 和 TensorFlow 加载模型。 + +``` python +def get_network(name, batch_size): + """获取网络的符号定义和随机权重""" + input_shape = (batch_size, 3, 224, 224) + output_shape = (batch_size, 1000) + + if "resnet" in name: + n_layer = int(name.split("-")[1]) + mod, params = relay.testing.resnet.get_workload( + num_layers=n_layer, batch_size=batch_size, dtype=dtype + ) + elif "vgg" in name: + n_layer = int(name.split("-")[1]) + mod, params = relay.testing.vgg.get_workload( + num_layers=n_layer, batch_size=batch_size, dtype=dtype + ) + elif name == "mobilenet": + mod, params = relay.testing.mobilenet.get_workload(batch_size=batch_size, dtype=dtype) + elif name == "squeezenet_v1.1": + mod, params = relay.testing.squeezenet.get_workload( + batch_size=batch_size, version="1.1", dtype=dtype + ) + elif name == "inception_v3": + input_shape = (batch_size, 3, 299, 299) + mod, params = relay.testing.inception_v3.get_workload(batch_size=batch_size, dtype=dtype) + elif name == "mxnet": + # MXNet 模型的示例 + from mxnet.gluon.model_zoo.vision import get_model + + block = get_model("resnet18_v1", pretrained=True) + mod, params = relay.frontend.from_mxnet(block, shape={"data": input_shape}, dtype=dtype) + net = mod["main"] + net = relay.Function( + net.params, relay.nn.softmax(net.body), None, net.type_params, net.attrs + ) + mod = tvm.IRModule.from_expr(net) + else: + raise ValueError("Unsupported network: " + name) + + return mod, params, input_shape, output_shape +``` + +## 设置调优选项 + +在调优之前,进行配置。 + +``` python +#### 设备配置 #### +target = tvm.target.cuda() + +#### 调优 OPTION #### +network = "resnet-18" +log_file = "%s.log" % network +dtype = "float32" + +tuning_option = { + "log_filename": log_file, + "tuner": "xgb", + "n_trial": 2000, + "early_stopping": 600, + "measure_option": autotvm.measure_option( + builder=autotvm.LocalBuilder(timeout=10), + runner=autotvm.LocalRunner(number=20, repeat=3, timeout=4, min_repeat_ms=150), + ), +} +``` + +输出结果: + +``` bash +/workspace/python/tvm/target/target.py:389: UserWarning: Try specifying cuda arch by adding 'arch=sm_xx' to your target. + warnings.warn("Try specifying cuda arch by adding 'arch=sm_xx' to your target.") +``` + +:::note +如何设置调优选项 + +通常,提供的默认值效果很好。 + +如果调优时间充足,可以把 `n_trial`,`early_stopping` 设置得大一些,就可以让调优运行的时间更长。 + +若有多个设备,可以用所有设备进行测试,以加快调优过程。(参阅下面的 `Scale up measurement` 部分)。 +::: + +## 开始调优 + +现在从网络中提取调优任务,并开始调优。接下来我们提供一个简单的实用函数。它只是一个初始实现,按顺序对任务列表进行调优。未来会引入更复杂的调优 scheduler。 + +``` python +# 可跳过此函数的实现。 +def tune_tasks( + tasks, + measure_option, + tuner="xgb", + n_trial=1000, + early_stopping=None, + log_filename="tuning.log", + use_transfer_learning=True, +): + # 创建 tmp 日志文件 + tmp_log_file = log_filename + ".tmp" + if os.path.exists(tmp_log_file): + os.remove(tmp_log_file) + + for i, tsk in enumerate(reversed(tasks)): + prefix = "[Task %2d/%2d] " % (i + 1, len(tasks)) + + # 创建调优器 + if tuner == "xgb" or tuner == "xgb-rank": + tuner_obj = XGBTuner(tsk, loss_type="rank") + elif tuner == "ga": + tuner_obj = GATuner(tsk, pop_size=100) + elif tuner == "random": + tuner_obj = RandomTuner(tsk) + elif tuner == "gridsearch": + tuner_obj = GridSearchTuner(tsk) + else: + raise ValueError("Invalid tuner: " + tuner) + + if use_transfer_learning: + if os.path.isfile(tmp_log_file): + tuner_obj.load_history(autotvm.record.load_from_file(tmp_log_file)) + + # 开始调优 + tsk_trial = min(n_trial, len(tsk.config_space)) + tuner_obj.tune( + n_trial=tsk_trial, + early_stopping=early_stopping, + measure_option=measure_option, + callbacks=[ + autotvm.callback.progress_bar(tsk_trial, prefix=prefix), + autotvm.callback.log_to_file(tmp_log_file), + ], + ) + + # 选择最佳记录到缓存文件 + autotvm.record.pick_best(tmp_log_file, log_filename) + os.remove(tmp_log_file) +``` + +最后启动调优作业,并评估端到端性能。 + +``` python +def tune_and_evaluate(tuning_opt): + # 从 relay 程序中提取工作负载 + print("Extract tasks...") + mod, params, input_shape, out_shape = get_network(network, batch_size=1) + tasks = autotvm.task.extract_from_program( + mod["main"], target=target, params=params, ops=(relay.op.get("nn.conv2d"),) + ) + + # 运行调优任务 + print("Tuning...") + tune_tasks(tasks, **tuning_opt) + + # 编译具有历史最佳记录的内核 + with autotvm.apply_history_best(log_file): + print("Compile...") + with tvm.transform.PassContext(opt_level=3): + lib = relay.build_module.build(mod, target=target, params=params) + + # 加载参数 + dev = tvm.device(str(target), 0) + module = runtime.GraphModule(lib["default"](dev)) + data_tvm = tvm.nd.array((np.random.uniform(size=input_shape)).astype(dtype)) + module.set_input("data", data_tvm) + + # 评估 + print("Evaluate inference time cost...") + print(module.benchmark(dev, number=1, repeat=600)) + + + +# 不在网页服务器中运行调优,因为它需要的时间太长。 +# 取消注释运行下一行 +# tune_and_evaluate(tuning_option) +``` + +## 样本输出 + +调优需要编译许多程序并从中提取特征,所以推荐使用高性能的 CPU。下面列出了一个输出示例。在 32T AMD Ryzen Threadripper 上大约耗时 4 小时才能看到以下输出,调优 target 是 NVIDIA 1080 Ti。(编译时会看到一些报错,若调优继续运行则可以忽略。) + +``` bash +Extract tasks... +Tuning... +[Task 1/12] Current/Best: 541.83/3570.66 GFLOPS | Progress: (960/2000) | 1001.31 s Done. +[Task 2/12] Current/Best: 0.56/ 803.33 GFLOPS | Progress: (704/2000) | 608.08 s Done. +[Task 3/12] Current/Best: 103.69/1141.25 GFLOPS | Progress: (768/2000) | 702.13 s Done. +[Task 4/12] Current/Best: 2905.03/3925.15 GFLOPS | Progress: (864/2000) | 745.94 sterminate called without an active exception +[Task 4/12] Current/Best: 2789.36/3925.15 GFLOPS | Progress: (1056/2000) | 929.40 s Done. +[Task 5/12] Current/Best: 89.06/1076.24 GFLOPS | Progress: (704/2000) | 601.73 s Done. +[Task 6/12] Current/Best: 40.39/2129.02 GFLOPS | Progress: (1088/2000) | 1125.76 s Done. +[Task 7/12] Current/Best: 4090.53/5007.02 GFLOPS | Progress: (800/2000) | 903.90 s Done. +[Task 8/12] Current/Best: 4.78/1272.28 GFLOPS | Progress: (768/2000) | 749.14 s Done. +[Task 9/12] Current/Best: 1391.45/2325.08 GFLOPS | Progress: (992/2000) | 1084.87 s Done. +[Task 10/12] Current/Best: 1995.44/2383.59 GFLOPS | Progress: (864/2000) | 862.60 s Done. +[Task 11/12] Current/Best: 4093.94/4899.80 GFLOPS | Progress: (224/2000) | 240.92 sterminate called without an active exception +[Task 11/12] Current/Best: 3487.98/4909.91 GFLOPS | Progress: (480/2000) | 534.96 sterminate called without an active exception +[Task 11/12] Current/Best: 4636.84/4912.17 GFLOPS | Progress: (1184/2000) | 1381.16 sterminate called without an active exception +[Task 11/12] Current/Best: 50.12/4912.17 GFLOPS | Progress: (1344/2000) | 1602.81 s Done. +[Task 12/12] Current/Best: 3581.31/4286.30 GFLOPS | Progress: (736/2000) | 943.52 s Done. +Compile... +Evaluate inference time cost... +Mean inference time (std dev): 1.07 ms (0.05 ms) +``` + +参考基线为 MXNet + TensorRT 在 ResNet-18 上的时间成本为 1.30ms,所以我们更快一点。 + +:::note +**遇到困难?** + +自调优模块容易出错,若总是看到「0.00/ 0.00 GFLOPS」,则表明存在问题。 + +首先确保设置了正确的设备配置,然后,在脚本开头添加如下行来打印调试信息,它将打印每个测试结果,可从中找到有用的错误消息。 + +``` python +import logging +logging.getLogger('autotvm').setLevel(logging.DEBUG) +``` + +随时在 https://discuss.tvm.apache.org 上向社区寻求帮助。 +::: + +## 使用多个设备加速测试 + +若有多个设备,可用所有设备进行测试。 TVM 使用 RPC Tracker(集中的控制器节点)来管理分布式设备,若有 10 个 GPU 卡,可以将它们全部注册到 tracker,并行运行 10 次测试,从而加快调优过程。 + +要启动 RPC tracker,在主机上运行如下命令。整个调优过程中都需要 tracker,因此需要为此命令打开一个新终端: + +``` bash +python -m tvm.exec.rpc_tracker --host=0.0.0.0 --port=9190 +``` + +预期输出: + +``` bash +INFO:RPCTracker:bind to 0.0.0.0:9190 +``` + +需要为每个设备打开一个新终端,启动一个 RPC 专用服务器。使用 key 来区分设备的类型。(注意:对于 rocm 后端,编译器存在一些内部错误,需要在参数列表中添加 --no-fork。) + +``` bash +python -m tvm.exec.rpc_server --tracker=127.0.0.1:9190 --key=1080ti +``` + +注册设备后,可以通过查询 rpc_tracker 来确认是否注册成功 + +``` bash +python -m tvm.exec.query_rpc_tracker --host=127.0.0.1 --port=9190 +``` + +比如有 4 个 1080 ti,2 个 titanx,1 个 gfx900,输出如下: + +``` bash +Queue Status +---------------------------------- +key total free pending +---------------------------------- +1080ti 4 4 0 +titanx 2 2 0 +gfx900 1 1 0 +---------------------------------- +``` + +最后,更改调优选项来使用 RPCRunner。用下面的代码替换上面的相应部分。 + +``` python +tuning_option = { + "log_filename": log_file, + "tuner": "xgb", + "n_trial": 2000, + "early_stopping": 600, + "measure_option": autotvm.measure_option( + builder=autotvm.LocalBuilder(timeout=10), + runner=autotvm.RPCRunner( + "1080ti", # change the device key to your key + "127.0.0.1", + 9190, + number=20, + repeat=3, + timeout=4, + min_repeat_ms=150, + ), + ), +} +``` + +[下载 Python 源代码:tune_relay_cuda.py](https://tvm.apache.org/docs/_downloads/0387f07dee851b2b8c6b73e3e88c3140/tune_relay_cuda.py) + +[下载 Jupyter Notebook:tune_relay_cuda.ipynb](https://tvm.apache.org/docs/_downloads/d1434e80dd27eef6b1c9cbaa13f1197b/tune_relay_cuda.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/autotune/03-autotuning_x86.md b/versioned_docs/version-0.12.0/how_to/autotune/03-autotuning_x86.md new file mode 100644 index 00000000..5d5326a8 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/autotune/03-autotuning_x86.md @@ -0,0 +1,264 @@ +--- +title: 为 x86 CPU 自动调优卷积网络 +--- + +# 为 x86 CPU 自动调优卷积网络 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/tune_with_autotvm/tune_relay_x86.html#sphx-glr-download-how-to-tune-with-autotvm-tune-relay-x86-py) 下载完整的示例代码 +::: + +**作者**:[Yao Wang](https://github.com/kevinthesun), [Eddie Yan](https://github.com/eqy) + +本文介绍如何为 x86 CPU 调优卷积神经网络。 + +注意,本教程不会在 Windows 或最新版本的 macOS 上运行。如需运行,请将本教程的主体放在 `if __name__ == "__main__":` 代码块中。 + +``` python +import os +import numpy as np + +import tvm +from tvm import relay, autotvm +from tvm.relay import testing +from tvm.autotvm.tuner import XGBTuner, GATuner, RandomTuner, GridSearchTuner +from tvm.autotvm.graph_tuner import DPTuner, PBQPTuner +import tvm.contrib.graph_executor as runtime +``` + +## 定义网络 + +首先在 Relay 前端 API 中定义网络,可以从 `relay.testing` 加载一些预定义的网络,也可以使用 Relay 构建 `relay.testing.resnet`。还可以从 MXNet、ONNX 和 TensorFlow 加载模型。 + +本教程用 resnet-18 作为调优示例。 + +``` python +def get_network(name, batch_size): + """获取网络的符号定义和随机权重""" + input_shape = (batch_size, 3, 224, 224) + output_shape = (batch_size, 1000) + + if "resnet" in name: + n_layer = int(name.split("-")[1]) + mod, params = relay.testing.resnet.get_workload( + num_layers=n_layer, batch_size=batch_size, dtype=dtype + ) + elif "vgg" in name: + n_layer = int(name.split("-")[1]) + mod, params = relay.testing.vgg.get_workload( + num_layers=n_layer, batch_size=batch_size, dtype=dtype + ) + elif name == "mobilenet": + mod, params = relay.testing.mobilenet.get_workload(batch_size=batch_size, dtype=dtype) + elif name == "squeezenet_v1.1": + mod, params = relay.testing.squeezenet.get_workload( + batch_size=batch_size, version="1.1", dtype=dtype + ) + elif name == "inception_v3": + input_shape = (batch_size, 3, 299, 299) + mod, params = relay.testing.inception_v3.get_workload(batch_size=batch_size, dtype=dtype) + elif name == "mxnet": + # MXNet 模型的示例 + from mxnet.gluon.model_zoo.vision import get_model + + block = get_model("resnet18_v1", pretrained=True) + mod, params = relay.frontend.from_mxnet(block, shape={input_name: input_shape}, dtype=dtype) + net = mod["main"] + net = relay.Function( + net.params, relay.nn.softmax(net.body), None, net.type_params, net.attrs + ) + mod = tvm.IRModule.from_expr(net) + else: + raise ValueError("Unsupported network: " + name) + + return mod, params, input_shape, output_shape + +# 将「llvm」替换为你的 CPU 的 target。 +# 例如,对于支持 Intel Xeon Platinum 8000 系列的 AWS EC2 c5 实例, +# target 是「llvm -mcpu=skylake-avx512」。 +# 对于支持 Intel Xeon E5-2666 v3 的 AWS EC2 c4 实例,target 是「llvm -mcpu=core-avx2」。 +target = "llvm" +batch_size = 1 +dtype = "float32" +model_name = "resnet-18" +log_file = "%s.log" % model_name +graph_opt_sch_file = "%s_graph_opt.log" % model_name + +# 设置图计算图的输入名称 +# 对于 ONNX 模型,它通常为“0”。 +input_name = "data" + +# 根据 CPU 内核设置调优的线程数 +num_threads = 1 +os.environ["TVM_NUM_THREADS"] = str(num_threads) +``` + +## 配置张量调优设置并创建任务 + +为了在 x86 CPU 上获得更好的内核执行性能,将卷积内核的数据布局从「NCHW」更改为「NCHWc」。为了处理这种情况,在 topi 中定义了 conv2d_NCHWc 算子,调优此算子而非普通的 conv2d。 + +使用本地模式来调优配置,RPC tracker 模式的设置类似于 [自动调优 ARM CPU 的卷积网络](autotuning_arm) 教程中的方法。 + +为了精准测试,应该多次重复测试,并取结果的平均值。此外,需要在重复测试时刷新权重张量的缓存,使得算子的测试延迟更接近其在端到端推理期间的实际延迟。 + +``` python +tuning_option = { + "log_filename": log_file, + "tuner": "random", + "early_stopping": None, + "measure_option": autotvm.measure_option( + builder=autotvm.LocalBuilder(), + runner=autotvm.LocalRunner( + number=1, repeat=10, min_repeat_ms=0, enable_cpu_cache_flush=True + ), + ), +} + +# 可跳过此函数的实现。 +def tune_kernels( + tasks, measure_option, tuner="gridsearch", early_stopping=None, log_filename="tuning.log" +): + for i, task in enumerate(tasks): + prefix = "[Task %2d/%2d] " % (i + 1, len(tasks)) + + # 创建调优器 + if tuner == "xgb" or tuner == "xgb-rank": + tuner_obj = XGBTuner(task, loss_type="rank") + elif tuner == "ga": + tuner_obj = GATuner(task, pop_size=50) + elif tuner == "random": + tuner_obj = RandomTuner(task) + elif tuner == "gridsearch": + tuner_obj = GridSearchTuner(task) + else: + raise ValueError("Invalid tuner: " + tuner) + + # 开始调优 + n_trial = len(task.config_space) + tuner_obj.tune( + n_trial=n_trial, + early_stopping=early_stopping, + measure_option=measure_option, + callbacks=[ + autotvm.callback.progress_bar(n_trial, prefix=prefix), + autotvm.callback.log_to_file(log_filename), + ], + ) + +# 使用 graph Tuner 实现计算图级别最优调度 +# 如果完成时间过长,则设置 use_DP=False。 +def tune_graph(graph, dshape, records, opt_sch_file, use_DP=True): + target_op = [ + relay.op.get("nn.conv2d"), + ] + Tuner = DPTuner if use_DP else PBQPTuner + executor = Tuner(graph, {input_name: dshape}, records, target_op, target) + executor.benchmark_layout_transform(min_exec_num=2000) + executor.run() + executor.write_opt_sch2record_file(opt_sch_file) +``` + +最后启动调优作业,并评估端到端性能。 + +``` python +def evaluate_performance(lib, data_shape): + # 上传参数到设备 + dev = tvm.cpu() + data_tvm = tvm.nd.array((np.random.uniform(size=data_shape)).astype(dtype)) + module = runtime.GraphModule(lib["default"](dev)) + module.set_input(input_name, data_tvm) + + # 评估 + print("Evaluate inference time cost...") + print(module.benchmark(dev, number=100, repeat=3)) + +def tune_and_evaluate(tuning_opt): + # 从 Relay 程序中提取工作负载 + print("Extract tasks...") + mod, params, data_shape, out_shape = get_network(model_name, batch_size) + tasks = autotvm.task.extract_from_program( + mod["main"], target=target, params=params, ops=(relay.op.get("nn.conv2d"),) + ) + + # 运行调优任务 + tune_kernels(tasks, **tuning_opt) + tune_graph(mod["main"], data_shape, log_file, graph_opt_sch_file) + + # 在默认模式下编译内核 + print("Evaluation of the network compiled in 'default' mode without auto tune:") + with tvm.transform.PassContext(opt_level=3): + print("Compile...") + lib = relay.build(mod, target=target, params=params) + evaluate_performance(lib, data_shape) + + # 在仅内核调优模式下编译内核 + print("\nEvaluation of the network been tuned on kernel level:") + with autotvm.apply_history_best(log_file): + print("Compile...") + with tvm.transform.PassContext(opt_level=3): + lib = relay.build(mod, target=target, params=params) + evaluate_performance(lib, data_shape) + + # 编译具有计算图级最佳记录的内核 + print("\nEvaluation of the network been tuned on graph level:") + with autotvm.apply_graph_best(graph_opt_sch_file): + print("Compile...") + with tvm.transform.PassContext(opt_level=3): + lib = relay.build_module.build(mod, target=target, params=params) + evaluate_performance(lib, data_shape) + +# 不在网页服务器中运行调优,因为它需要的时间太长。 +# 取消注释运行下一行 +# tune_and_evaluate(tuning_option) +``` + +## 样本输出 + +调优需要编译许多程序,并从中提取特征,推荐使用高性能的 CPU。下面列出了一个输出示例。 + +``` bash +Extract tasks... +Tuning... +[Task 1/12] Current/Best: 598.05/2497.63 GFLOPS | Progress: (252/252) | 1357.95 s Done. +[Task 2/12] Current/Best: 522.63/2279.24 GFLOPS | Progress: (784/784) | 3989.60 s Done. +[Task 3/12] Current/Best: 447.33/1927.69 GFLOPS | Progress: (784/784) | 3869.14 s Done. +[Task 4/12] Current/Best: 481.11/1912.34 GFLOPS | Progress: (672/672) | 3274.25 s Done. +[Task 5/12] Current/Best: 414.09/1598.45 GFLOPS | Progress: (672/672) | 2720.78 s Done. +[Task 6/12] Current/Best: 508.96/2273.20 GFLOPS | Progress: (768/768) | 3718.75 s Done. +[Task 7/12] Current/Best: 469.14/1955.79 GFLOPS | Progress: (576/576) | 2665.67 s Done. +[Task 8/12] Current/Best: 230.91/1658.97 GFLOPS | Progress: (576/576) | 2435.01 s Done. +[Task 9/12] Current/Best: 487.75/2295.19 GFLOPS | Progress: (648/648) | 3009.95 s Done. +[Task 10/12] Current/Best: 182.33/1734.45 GFLOPS | Progress: (360/360) | 1755.06 s Done. +[Task 11/12] Current/Best: 372.18/1745.15 GFLOPS | Progress: (360/360) | 1684.50 s Done. +[Task 12/12] Current/Best: 215.34/2271.11 GFLOPS | Progress: (400/400) | 2128.74 s Done. +INFO Start to benchmark layout transformation... +INFO Benchmarking layout transformation successful. +INFO Start to run dynamic programming algorithm... +INFO Start forward pass... +INFO Finished forward pass. +INFO Start backward pass... +INFO Finished backward pass... +INFO Finished DPExecutor run. +INFO Writing optimal schedules to resnet-18_graph_opt.log successfully. + +Evaluation of the network compiled in 'default' mode without auto tune: +Compile... +Evaluate inference time cost... +Mean inference time (std dev): 4.5 ms (0.03 ms) + +Evaluation of the network been tuned on kernel level: +Compile... +Evaluate inference time cost... +Mean inference time (std dev): 3.2 ms (0.03 ms) + +Evaluation of the network been tuned on graph level: +Compile... +Config for target=llvm -keys=cpu -link-params=0, workload=('dense_nopack.x86', ('TENSOR', (1, 512), 'float32'), ('TENSOR', (1000, 512), 'float32'), None, 'float32') is missing in ApplyGraphBest context. A fallback configuration is used, which may bring great performance regression. +Config for target=llvm -keys=cpu -link-params=0, workload=('dense_pack.x86', ('TENSOR', (1, 512), 'float32'), ('TENSOR', (1000, 512), 'float32'), None, 'float32') is missing in ApplyGraphBest context. A fallback configuration is used, which may bring great performance regression. +Evaluate inference time cost... +Mean inference time (std dev): 3.16 ms (0.03 ms) +``` + +[下载 Python 源代码:tune_relay_x86.py](https://tvm.apache.org/docs/_downloads/6836ce26807b8d33b8f499287c1f3d04/tune_relay_x86.py) + +[下载 Jupyter notebook:tune_relay_x86.ipynb](https://tvm.apache.org/docs/_downloads/910e6ecee4ecac8d8ca0baeb6d00689d/tune_relay_x86.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/autotune/04-autotuning_arm.md b/versioned_docs/version-0.12.0/how_to/autotune/04-autotuning_arm.md new file mode 100644 index 00000000..a9f20129 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/autotune/04-autotuning_arm.md @@ -0,0 +1,352 @@ +--- +title: 为 ARM CPU 自动调优卷积网络 +--- + +# 为 ARM CPU 自动调优卷积网络 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/tune_with_autotvm/tune_relay_arm.html#sphx-glr-download-how-to-tune-with-autotvm-tune-relay-arm-py) 下载完整的示例代码 +::: + +**作者**:[Lianmin Zheng](https://github.com/merrymercy), [Zhao Wu](https://github.com/FrozenGene), [Eddie Yan](https://github.com/eqy) + +针对特定 ARM 设备的自动调优对于获得最佳性能至关重要,本文介绍如何调优整个卷积网络。 + +TVM 中 ARM CPU 的算子实现是以 template 形式编写的,该 template 有许多可调参数(tile 因子,vectorization,unrolling等)。对神经网络中的所有卷积和深度卷积算子调优后,会生成一个日志文件,它存储所有必需算子的最佳参数值。当 TVM 编译器编译这些算子时,会查询这个日志文件,从而获取最佳参数值。 + +我们还发布了一些 ARM 设备的预调参数。可以前往 [ARM CPU Benchmark](https://github.com/apache/tvm/wiki/Benchmark#arm-cpu) 查看结果。 + +注意,本教程无法在 Windows 或最新版本的 macOS 上运行。若要运行,需要将本教程的主体包装在 `if __name__ == "__main__":` 块中。 + +## 安装依赖 + +要在 TVM 中使用 autotvm 包,需要安装额外的依赖(如果用的是 Python2,请将「3」更改为「2」): + +``` bash +pip3 install --user psutil xgboost tornado cloudpickle +``` + +为了让 TVM 在调优中运行更快,推荐使用 Cython 作为 TVM 的 FFI。在 TVM 的根目录下,执行如下命令:(若使用 Python2,将「3」改为「2」): + +``` bash +pip3 install --user cython +sudo make cython3 +``` + +在 Python 代码中导入包: + +``` python +import os +import numpy as np + +import tvm +from tvm import relay, autotvm +import tvm.relay.testing +from tvm.autotvm.tuner import XGBTuner, GATuner, RandomTuner, GridSearchTuner +from tvm.contrib.utils import tempdir +import tvm.contrib.graph_executor as runtime +``` + +## 定义网络 + +首先要在 relay 前端 API 中定义网络,可以从 `relay.testing` 加载一些预定义的网络,还可以从 MXNet、ONNX 和 TensorFlow 加载模型。 + +``` python +def get_network(name, batch_size): + """获取网络的符号定义和随机权重""" + input_shape = (batch_size, 3, 224, 224) + output_shape = (batch_size, 1000) + + if "resnet" in name: + n_layer = int(name.split("-")[1]) + mod, params = relay.testing.resnet.get_workload( + num_layers=n_layer, batch_size=batch_size, dtype=dtype + ) + elif "vgg" in name: + n_layer = int(name.split("-")[1]) + mod, params = relay.testing.vgg.get_workload( + num_layers=n_layer, batch_size=batch_size, dtype=dtype + ) + elif name == "mobilenet": + mod, params = relay.testing.mobilenet.get_workload(batch_size=batch_size) + elif name == "squeezenet_v1.1": + mod, params = relay.testing.squeezenet.get_workload( + batch_size=batch_size, version="1.1", dtype=dtype + ) + elif name == "inception_v3": + input_shape = (batch_size, 3, 299, 299) + mod, params = relay.testing.inception_v3.get_workload(batch_size=batch_size, dtype=dtype) + elif name == "mxnet": + # MXNet 模型的示例 + from mxnet.gluon.model_zoo.vision import get_model + + block = get_model("resnet18_v1", pretrained=True) + mod, params = relay.frontend.from_mxnet(block, shape={"data": input_shape}, dtype=dtype) + net = mod["main"] + net = relay.Function( + net.params, relay.nn.softmax(net.body), None, net.type_params, net.attrs + ) + mod = tvm.IRModule.from_expr(net) + else: + raise ValueError("Unsupported network: " + name) + + return mod, params, input_shape, output_shape +``` + +## 启动 RPC Tracker + +TVM 使用 RPC session 与 ARM 板进行通信,在调优期间,调优器会将生成的代码发送到板上并测试板上代码的速度。 + +为了加速调优,TVM 使用 RPC Tracker(集中的控制器节点)来管理分布式设备。例如,若有 10 部手机,可以将它们全部注册到 Tracker,并行运行 10 次测试,从而加快调优过程。 + +要启动 RPC tracker,在主机上运行如下命令。在整个调优过程中都需要 tracker,因此需要为此命令打开一个新终端: + +``` bash +python -m tvm.exec.rpc_tracker --host=0.0.0.0 --port=9190 +``` + +预期输出: + +``` bash +INFO:RPCTracker:bind to 0.0.0.0:9190 +``` + +## 将设备注册到 RPC Tracker + +接下来把设备注册到 Tracker。第一步是为 ARM 设备构建 TVM runtime。 + +* 对于 Linux:按照 [在设备上构建 TVM Runtime](https://tvm.apache.org/docs/how_to/deploy_models/deploy_model_on_rasp.html#build-tvm-runtime-on-device) 教程操作,然后将设备注册到 Tracker + + ``` python + python -m tvm.exec.rpc_server --tracker=[HOST_IP]:9190 --key=rk3399 + ``` + + (将 `[HOST_IP]` 换为你的主机 IP 地址) + +* 对于 Android:按照此 [说明](https://github.com/apache/tvm/tree/main/apps/android_rpc) 在 Android 设备上安装 TVM RPC APK,确保可以通过 Android rpc 测试。在调优期间,打开手机开发者选项并勾选「在更改期间保持屏幕唤醒」,为手机接通电源。 + +注册设备后,通过查询 rpc_tracker 来确认是否注册成功 + +``` bash +python -m tvm.exec.query_rpc_tracker --host=0.0.0.0 --port=9190 +``` + +例如,如果有 2 台华为 mate10 pro、11 台树莓派 3B 和 2 台 rk3399,则输出是 + +``` bash +Queue Status +---------------------------------- +key total free pending +---------------------------------- +mate10pro 2 2 0 +rk3399 2 2 0 +rpi3b 11 11 0 +---------------------------------- +``` + +将多个设备注册到 tracker,从而加快调优测试。 + +## 设置调优选项 + +在调优之前,进行配置。这里以 RK3399 板为例。根据自己的设备修改 target 和 device_key。若用 Android 手机,请将 `use_android` 设置为 True。 + +``` python +#### 设备配置 #### +# 将 "aarch64-linux-gnu" 替换为单板的正确 target。 +# 此 target 用于交叉编译。可以通过:code:`gcc -v` 来查询。 +target = tvm.target.Target("llvm -device=arm_cpu -mtriple=aarch64-linux-gnu") + +# 根据设备替换 device_key 的值 +device_key = "rk3399" + +# 若使用 Android 手机,设置 use_android 为 True +use_android = False + +#### 调优选项 #### +network = "resnet-18" +log_file = "%s.%s.log" % (device_key, network) +dtype = "float32" + +tuning_option = { + "log_filename": log_file, + "tuner": "xgb", + "n_trial": 1500, + "early_stopping": 800, + "measure_option": autotvm.measure_option( + builder=autotvm.LocalBuilder(build_func="ndk" if use_android else "default"), + runner=autotvm.RPCRunner( + device_key, + host="127.0.0.1", + port=9190, + number=5, + timeout=10, + ), + ), +} +``` + +:::note +#### 如何设置调优选项 +通常,提供的默认值效果很好。 + +若模型包含深度卷积,将 `try_spatial_pack_depthwise` 设置为 `True`,这会使得执行效果优于默认优化。例如,在 ARM CPU A53 2.0GHz 上,可以将 Mobilenet V1 模型的深度卷积性能提高 1.6 倍。 +::: + +## 开始调优 + +下面开始从网络中提取调优任务,并开始调优。接下来我们提供一个简单的实用函数。它只是一个初始实现,按顺序对任务列表进行调优。未来会引入更复杂的调优 scheduler。 + +``` python +# 可跳过此函数的实现。 +def tune_tasks( + tasks, + measure_option, + tuner="xgb", + n_trial=1000, + early_stopping=None, + log_filename="tuning.log", + use_transfer_learning=True, +): + # 创建 tmp 日志文件 + tmp_log_file = log_filename + ".tmp" + if os.path.exists(tmp_log_file): + os.remove(tmp_log_file) + + for i, tsk in enumerate(reversed(tasks)): + prefix = "[Task %2d/%2d] " % (i + 1, len(tasks)) + + # 创建调优器 + if tuner == "xgb" or tuner == "xgb-rank": + tuner_obj = XGBTuner(tsk, loss_type="rank") + elif tuner == "xgb_knob": + tuner_obj = XGBTuner(tsk, loss_type="rank", feature_type="knob") + elif tuner == "xgb_itervar": + tuner_obj = XGBTuner(tsk, loss_type="rank", feature_type="itervar") + elif tuner == "xgb_curve": + tuner_obj = XGBTuner(tsk, loss_type="rank", feature_type="curve") + elif tuner == "ga": + tuner_obj = GATuner(tsk, pop_size=50) + elif tuner == "random": + tuner_obj = RandomTuner(tsk) + elif tuner == "gridsearch": + tuner_obj = GridSearchTuner(tsk) + else: + raise ValueError("Invalid tuner: " + tuner) + + if use_transfer_learning: + if os.path.isfile(tmp_log_file): + tuner_obj.load_history(autotvm.record.load_from_file(tmp_log_file)) + + # 开始调优 + tsk_trial = min(n_trial, len(tsk.config_space)) + tuner_obj.tune( + n_trial=tsk_trial, + early_stopping=early_stopping, + measure_option=measure_option, + callbacks=[ + autotvm.callback.progress_bar(tsk_trial, prefix=prefix), + autotvm.callback.log_to_file(tmp_log_file), + ], + ) + + # 选择最佳记录到缓存文件 + autotvm.record.pick_best(tmp_log_file, log_filename) + os.remove(tmp_log_file) +``` + +最后启动调优任务,并评估端到端性能。 + +``` python +def tune_and_evaluate(tuning_opt): + # 从 relay 程序中提取工作负载 + print("Extract tasks...") + mod, params, input_shape, _ = get_network(network, batch_size=1) + tasks = autotvm.task.extract_from_program( + mod["main"], target=target, params=params, ops=(relay.op.get("nn.conv2d"),) + ) + + # 运行调优任务 + print("Tuning...") + tune_tasks(tasks, **tuning_opt) + + # 编译具有历史最佳记录的内核 + with autotvm.apply_history_best(log_file): + print("Compile...") + with tvm.transform.PassContext(opt_level=3): + lib = relay.build_module.build(mod, target=target, params=params) + + # 导出库 + tmp = tempdir() + if use_android: + from tvm.contrib import ndk + + filename = "net.so" + lib.export_library(tmp.relpath(filename), ndk.create_shared) + else: + filename = "net.tar" + lib.export_library(tmp.relpath(filename)) + + # 上传模块到设备 + print("Upload...") + remote = autotvm.measure.request_remote(device_key, "127.0.0.1", 9190, timeout=10000) + remote.upload(tmp.relpath(filename)) + rlib = remote.load_module(filename) + + # 上传参数到设备 + dev = remote.device(str(target), 0) + module = runtime.GraphModule(rlib["default"](dev)) + data_tvm = tvm.nd.array((np.random.uniform(size=input_shape)).astype(dtype)) + module.set_input("data", data_tvm) + + # 评估 + print("Evaluate inference time cost...") + print(module.benchmark(dev, number=1, repeat=10)) + +# 不在网页服务器中运行调优,因为它耗时很久。 +# 取消注释运行下一行 +# tune_and_evaluate(tuning_option) +``` + +## 样本输出 + +调优需要编译许多程序,并从中提取特征,所以推荐使用高性能的 CPU。下面列出了一个输出示例。在 32T AMD Ryzen Threadripper 设备上,大约耗时 2 个小时。 + +``` bash +Extract tasks... +Tuning... +[Task 1/12] Current/Best: 22.37/ 52.19 GFLOPS | Progress: (544/1000) | 406.59 s Done. +[Task 2/12] Current/Best: 6.51/ 18.77 GFLOPS | Progress: (608/1000) | 325.05 s Done. +[Task 3/12] Current/Best: 4.67/ 24.87 GFLOPS | Progress: (480/1000) | 372.31 s Done. +[Task 4/12] Current/Best: 11.35/ 46.83 GFLOPS | Progress: (736/1000) | 602.39 s Done. +[Task 5/12] Current/Best: 1.01/ 19.80 GFLOPS | Progress: (448/1000) | 262.16 s Done. +[Task 6/12] Current/Best: 2.47/ 23.76 GFLOPS | Progress: (672/1000) | 563.85 s Done. +[Task 7/12] Current/Best: 14.57/ 33.97 GFLOPS | Progress: (544/1000) | 465.15 s Done. +[Task 8/12] Current/Best: 1.13/ 17.65 GFLOPS | Progress: (576/1000) | 365.08 s Done. +[Task 9/12] Current/Best: 14.45/ 22.66 GFLOPS | Progress: (928/1000) | 724.25 s Done. +[Task 10/12] Current/Best: 3.22/ 15.36 GFLOPS | Progress: (864/1000) | 564.27 s Done. +[Task 11/12] Current/Best: 11.03/ 32.23 GFLOPS | Progress: (736/1000) | 635.15 s Done. +[Task 12/12] Current/Best: 8.00/ 21.65 GFLOPS | Progress: (1000/1000) | 1111.81 s Done. +Compile... +Upload... +Evaluate inference time cost... +Mean inference time (std dev): 162.59 ms (0.06 ms) +``` + +:::note +**遇到困难?** + +自调优模块容易出错,若总是看到「0.00/ 0.00 GFLOPS」,则表示存在问题。 + +首先确保设置了正确的设备配置,然后,在脚本开头添加如下行来打印调试信息,它将打印每个测试结果,可从中找到有用的报错消息。 + +``` python +import logging +logging.getLogger('autotvm').setLevel(logging.DEBUG) +``` + +你也可以随时访问 https://discuss.tvm.apache.org 社区寻求帮助 +::: + +[下载 Python 源代码:tune_relay_arm.py](https://tvm.apache.org/docs/_downloads/35eacf8f75629e07aeda1329bdb7d53c/tune_relay_arm.py) + +[下载 Jupyter Notebook:tune_relay_arm.ipynb](https://tvm.apache.org/docs/_downloads/bc33c0d33026b287306b6ead1a50b04a/tune_relay_arm.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/autotune/05-autotuning_mobile.md b/versioned_docs/version-0.12.0/how_to/autotune/05-autotuning_mobile.md new file mode 100644 index 00000000..0333434a --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/autotune/05-autotuning_mobile.md @@ -0,0 +1,348 @@ +--- +title: 为 Mobile GPU 自动调优卷积网络 +--- + +# 为 Mobile GPU 自动调优卷积网络 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/tune_with_autotvm/tune_relay_mobile_gpu.html#sphx-glr-download-how-to-tune-with-autotvm-tune-relay-mobile-gpu-py) 下载完整的示例代码 +::: + +**作者**:[Lianmin Zheng](https://github.com/merrymercy), [Eddie Yan](https://github.com/eqy) + +针对特定设备的自动调优对于获得最佳性能至关重要。本文介绍如何调优整个卷积网络。 + +TVM 中 Mobile GPU 的算子实现是以 template 形式编写的。该 template 有许多可调参数(tile 因子,vectorization,unrolling 等)。对神经网络中的所有卷积、深度卷积和密集算子调优后,会生成一个日志文件,它存储所有必需算子的最佳参数值。当 TVM 编译器编译这些算子时,将查询此日志文件以获取最佳参数值。 + +我们还发布了一些 arm 设备的预调参数,可以前往 [Mobile GPU Benchmark](https://github.com/apache/tvm/wiki/Benchmark#mobile-gpu) 查看详细信息。 + +注意,本教程无法在 Windows 或最新版本的 macOS 上运行。若要运行,需要将本教程的主体包装在 `if __name__ == "__main__":` 块中。 + +## 安装依赖 + +要在 TVM 中使用 autotvm 包,需要安装额外的依赖(如果用的是 Python2,请将「3」更改为「2」): + +``` bash +pip3 install --user psutil xgboost tornado cloudpickle +``` + +为了让 TVM 在调优过程中运行更快,推荐使用 Cython 作为 TVM 的 FFI。在 TVM 的根目录下,执行如下命令:(若使用 Python2,将「3」改为「2」): + +``` bash +pip3 install --user cython +sudo make cython3 +``` + +在 Python 代码中导入包: + +``` python +import os +import numpy as np + +import tvm +from tvm import relay, autotvm +import tvm.relay.testing +from tvm.autotvm.tuner import XGBTuner, GATuner, RandomTuner, GridSearchTuner +from tvm.contrib.utils import tempdir +import tvm.contrib.graph_executor as runtime +``` + +## 定义网络 + +首先要在 Relay 前端 API 中定义网络,可以从 `relay.testing` 加载一些预定义的网络,也可以从 MXNet、ONNX 和 TensorFlow 加载模型。 + +``` python +def get_network(name, batch_size): + """获取网络的符号定义和随机权重""" + input_shape = (batch_size, 3, 224, 224) + output_shape = (batch_size, 1000) + + if "resnet" in name: + n_layer = int(name.split("-")[1]) + mod, params = relay.testing.resnet.get_workload( + num_layers=n_layer, batch_size=batch_size, dtype=dtype + ) + elif "vgg" in name: + n_layer = int(name.split("-")[1]) + mod, params = relay.testing.vgg.get_workload( + num_layers=n_layer, batch_size=batch_size, dtype=dtype + ) + elif name == "mobilenet": + mod, params = relay.testing.mobilenet.get_workload(batch_size=batch_size, dtype=dtype) + elif name == "squeezenet_v1.1": + mod, params = relay.testing.squeezenet.get_workload( + batch_size=batch_size, version="1.1", dtype=dtype + ) + elif name == "inception_v3": + input_shape = (batch_size, 3, 299, 299) + mod, params = relay.testing.inception_v3.get_workload(batch_size=batch_size, dtype=dtype) + elif name == "mxnet": + # MXNet 模型的示例 + from mxnet.gluon.model_zoo.vision import get_model + + block = get_model("resnet18_v1", pretrained=True) + mod, params = relay.frontend.from_mxnet(block, shape={"data": input_shape}, dtype=dtype) + net = mod["main"] + net = relay.Function( + net.params, relay.nn.softmax(net.body), None, net.type_params, net.attrs + ) + mod = tvm.IRModule.from_expr(net) + else: + raise ValueError("Unsupported network: " + name) + + return mod, params, input_shape, output_shape +``` + +## 启动 RPC Tracker + +TVM 使用 RPC session 与 ARM 板进行通信,在调优期间,调优器会将生成的代码发送到板上并测试板上代码的速度。 + +为了加速调优,TVM 使用 RPC Tracker(集中的控制器节点)来管理分布式设备。例如,若有 10 部手机,可以将它们全部注册到 Tracker,并行运行 10 次测试,从而加快调优过程。 + +在整个调优过程中都需要 tracker,因此需要为此命令打开一个新终端,在主机上运行如下命令启动 RPC tracker: + +``` bash +python -m tvm.exec.rpc_tracker --host=0.0.0.0 --port=9190 +``` + +预期输出: + +``` bash +INFO:RPCTracker:bind to 0.0.0.0:9190 +``` + +## 将设备注册到 RPC Tracker + +接下来把设备注册到 Tracker。第一步是为 ARM 设备构建 TVM runtime 。 + +* 对于 Linux:按照 [在设备上构建 TVM Runtime](https://tvm.apache.org/docs/how_to/deploy_models/deploy_model_on_rasp.html#build-tvm-runtime-on-device) 教程操作,然后将设备注册到 Tracker + + ``` python + python -m tvm.exec.rpc_server --tracker=[HOST_IP]:9190 --key=rk3399 + ``` + + (将 `[HOST_IP]` 替换为你的主机的 IP 地址) + +* 对于 Android:按照此 [说明](https://github.com/apache/tvm/tree/main/apps/android_rpc) 在 Android 设备上安装 TVM RPC APK,确保可以通过 Android rpc 测试。在调优期间,打开手机开发者选项并勾选「在更改期间保持屏幕唤醒」,为手机接通电源。 + +注册设备后,通过查询 rpc_tracker 来确认是否注册成功 + +``` bash +python -m tvm.exec.query_rpc_tracker --host=0.0.0.0 --port=9190 +``` + +例如,如果有 2 台华为 mate10 pro、11 台树莓派 3B 和 2 台 rk3399,则输出是 + +``` bash +Queue Status +---------------------------------- +key total free pending +---------------------------------- +mate10pro 2 2 0 +rk3399 2 2 0 +rpi3b 11 11 0 +---------------------------------- +``` + +将多个设备注册到 tracker,从而加快调优测试。 + +## 设置调优选项 + +在调优之前,需要先进行配置。这里以 RK3399 板为例。根据自己的设备修改 target 和 device_key。若用 Android 手机,请将 `use_android` 设置为 True。 + +``` python +#### 设备配置 #### +# 将 "aarch64-linux-gnu" 替换为你的板子的正确 target。 +# 此 target 用于交叉编译。可以通过:code:`gcc -v` 来查询。 +target = tvm.target.Target("opencl -device=mali", host="llvm -mtriple=aarch64-linux-gnu") + +# 根据设备替换 device_key 的值 +device_key = "rk3399" + +# 若使用 Android 手机,设置 use_android 为 True +use_android = False + +#### 调优选项 #### +network = "resnet-18" +log_file = "%s.%s.log" % (device_key, network) +dtype = "float32" + +tuning_option = { + "log_filename": log_file, + "tuner": "xgb", + "n_trial": 1000, + "early_stopping": 450, + "measure_option": autotvm.measure_option( + builder=autotvm.LocalBuilder(build_func="ndk" if use_android else "default"), + runner=autotvm.RPCRunner( + device_key, + host="127.0.0.1", + port=9190, + number=10, + timeout=5, + ), + ), +} +``` + +:::note +#### 如何设置调优选项 +通常,提供的默认值效果就不错。如果调优时间充足,可以把 `n_trial`,`early_stopping` 设置得大一些,让调优运行的时间更长。若设备运行速度非常慢,或者 conv2d 算子有很多 GFLOP,把 timeout 设置大一点。 +::: +## 开始调优 + +下面开始从网络中提取调优任务,并开始调优。接下来我们提供一个简单的实用函数。它只是一个初始实现,按顺序对任务列表进行调优。未来会引入更复杂的调优 scheduler。 + +``` python +# 可跳过此函数的实现。 +def tune_tasks( + tasks, + measure_option, + tuner="xgb", + n_trial=1000, + early_stopping=None, + log_filename="tuning.log", + use_transfer_learning=True, +): + # 创建 tmp 日志文件 + tmp_log_file = log_filename + ".tmp" + if os.path.exists(tmp_log_file): + os.remove(tmp_log_file) + + for i, tsk in enumerate(reversed(tasks)): + prefix = "[Task %2d/%2d] " % (i + 1, len(tasks)) + + # 创建调优器 + if tuner == "xgb" or tuner == "xgb-rank": + tuner_obj = XGBTuner(tsk, loss_type="rank") + elif tuner == "ga": + tuner_obj = GATuner(tsk, pop_size=50) + elif tuner == "random": + tuner_obj = RandomTuner(tsk) + elif tuner == "gridsearch": + tuner_obj = GridSearchTuner(tsk) + else: + raise ValueError("Invalid tuner: " + tuner) + + if use_transfer_learning: + if os.path.isfile(tmp_log_file): + tuner_obj.load_history(autotvm.record.load_from_file(tmp_log_file)) + + # 开始调优 + tsk_trial = min(n_trial, len(tsk.config_space)) + tuner_obj.tune( + n_trial=tsk_trial, + early_stopping=early_stopping, + measure_option=measure_option, + callbacks=[ + autotvm.callback.progress_bar(tsk_trial, prefix=prefix), + autotvm.callback.log_to_file(tmp_log_file), + ], + ) + + # 选择最佳记录到缓存文件 + autotvm.record.pick_best(tmp_log_file, log_filename) + os.remove(tmp_log_file) +``` + +最后启动调优任务,并评估端到端性能。 + +``` python +def tune_and_evaluate(tuning_opt): + # 从 Relay 程序中提取工作负载 + print("Extract tasks...") + mod, params, input_shape, _ = get_network(network, batch_size=1) + tasks = autotvm.task.extract_from_program( + mod["main"], + target=target, + params=params, + ops=(relay.op.get("nn.conv2d"),), + ) + + # 运行调优任务 + print("Tuning...") + tune_tasks(tasks, **tuning_opt) + + # 编译具有历史最佳记录的内核 + with autotvm.apply_history_best(log_file): + print("Compile...") + with tvm.transform.PassContext(opt_level=3): + lib = relay.build_module.build(mod, target=target, params=params) + # 导出库 + tmp = tempdir() + if use_android: + from tvm.contrib import ndk + + filename = "net.so" + lib.export_library(tmp.relpath(filename), ndk.create_shared) + else: + filename = "net.tar" + lib.export_library(tmp.relpath(filename)) + + # 上传模块到设备 + print("Upload...") + remote = autotvm.measure.request_remote(device_key, "127.0.0.1", 9190, timeout=10000) + remote.upload(tmp.relpath(filename)) + rlib = remote.load_module(filename) + + # 上传参数到设备 + dev = remote.device(str(target), 0) + module = runtime.GraphModule(rlib["default"](dev)) + data_tvm = tvm.nd.array((np.random.uniform(size=input_shape)).astype(dtype)) + module.set_input("data", data_tvm) + + # 评估 + print("Evaluate inference time cost...") + print(module.benchmark(dev, number=1, repeat=30)) + +# 不在网页服务器中运行调优,因为它需要的时间太长。 +# 取消注释运行下一行 +# tune_and_evaluate(tuning_option) +``` + +## 样本输出 + +调优需要编译许多程序,并从中提取特征,所以推荐使用高性能的 CPU。下面列出了一个输出示例。在 32T AMD Ryzen Threadripper 设备上,大约需要 3 个小时。 + +``` bash +Extract tasks... +Tuning... +[Task 1/17] Current/Best: 25.30/ 39.12 GFLOPS | Progress: (992/1000) | 751.22 s Done. +[Task 2/17] Current/Best: 40.70/ 45.50 GFLOPS | Progress: (736/1000) | 545.46 s Done. +[Task 3/17] Current/Best: 38.83/ 42.35 GFLOPS | Progress: (992/1000) | 1549.85 s Done. +[Task 4/17] Current/Best: 23.31/ 31.02 GFLOPS | Progress: (640/1000) | 1059.31 s Done. +[Task 5/17] Current/Best: 0.06/ 2.34 GFLOPS | Progress: (544/1000) | 305.45 s Done. +[Task 6/17] Current/Best: 10.97/ 17.20 GFLOPS | Progress: (992/1000) | 1050.00 s Done. +[Task 7/17] Current/Best: 8.98/ 10.94 GFLOPS | Progress: (928/1000) | 421.36 s Done. +[Task 8/17] Current/Best: 4.48/ 14.86 GFLOPS | Progress: (704/1000) | 582.60 s Done. +[Task 9/17] Current/Best: 10.30/ 25.99 GFLOPS | Progress: (864/1000) | 899.85 s Done. +[Task 10/17] Current/Best: 11.73/ 12.52 GFLOPS | Progress: (608/1000) | 304.85 s Done. +[Task 11/17] Current/Best: 15.26/ 18.68 GFLOPS | Progress: (800/1000) | 747.52 s Done. +[Task 12/17] Current/Best: 17.48/ 26.71 GFLOPS | Progress: (1000/1000) | 1166.40 s Done. +[Task 13/17] Current/Best: 0.96/ 11.43 GFLOPS | Progress: (960/1000) | 611.65 s Done. +[Task 14/17] Current/Best: 17.88/ 20.22 GFLOPS | Progress: (672/1000) | 670.29 s Done. +[Task 15/17] Current/Best: 11.62/ 13.98 GFLOPS | Progress: (736/1000) | 449.25 s Done. +[Task 16/17] Current/Best: 19.90/ 23.83 GFLOPS | Progress: (608/1000) | 708.64 s Done. +[Task 17/17] Current/Best: 17.98/ 22.75 GFLOPS | Progress: (736/1000) | 1122.60 s Done. +Compile... +Upload... +Evaluate inference time cost... +Mean inference time (std dev): 128.05 ms (7.74 ms) +``` + +:::note +**遇到困难?** + +自动调优模块容易出错,若总是看到「0.00/ 0.00 GFLOPS」,则意味着有问题。首先确保设置了正确的设备配置,然后,在脚本开头添加如下行来打印调试信息,打印所有测试结果,可从中找到有用的报错消息。 + +``` python +import logging +logging.getLogger('autotvm').setLevel(logging.DEBUG) +``` + +随时在 https://discuss.tvm.apache.org 上向社区寻求帮助。 +::: + +[下载 Python 源代码:tune_relay_mobile_gpu.py](https://tvm.apache.org/docs/_downloads/644d28fc67dfb3099fb0d275ffcf1c7c/tune_relay_mobile_gpu.py) + +[下载 Jupyter Notebook:tune_relay_mobile_gpu.ipynb](https://tvm.apache.org/docs/_downloads/6b0f549107f73f2e48c894372be08bcb/tune_relay_mobile_gpu.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/autotune/_category_.json b/versioned_docs/version-0.12.0/how_to/autotune/_category_.json new file mode 100644 index 00000000..dd5ae35c --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/autotune/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 160 +} diff --git a/versioned_docs/version-0.12.0/how_to/autotune/index.md b/versioned_docs/version-0.12.0/how_to/autotune/index.md new file mode 100644 index 00000000..cf5de43b --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/autotune/index.md @@ -0,0 +1,11 @@ +--- +title: 使用模板和 AutoTVM 进行自动调优 +--- + +AutoTVM 通过提供模板调度和搜索模板定义的参数空间,提供了一种模型和算子调优的方法。以下操作指南将演示如何编写模板调度,并针对各种不同的硬件平台进行优化: + +* [在 NVIDIA GPU 上调优高性能卷积](/docs/how_to/autotune/tuning_nvidia) +* [为 NVIDIA GPU 自动调优卷积网络](/docs/how_to/autotune/autotuning_nvidia) +* [为 x86 CPU 自动调优卷积网络](/docs/how_to/autotune/autotuning_x86) +* [为 ARM CPU 自动调优卷积网络](/docs/how_to/autotune/autotuning_arm) +* [为移动平台 GPU 自动调优卷积网络](/docs/how_to/autotune/autotuning_mobile) diff --git a/versioned_docs/version-0.12.0/how_to/compile/01-compile_pytorch.md b/versioned_docs/version-0.12.0/how_to/compile/01-compile_pytorch.md new file mode 100644 index 00000000..dcfb2377 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/compile/01-compile_pytorch.md @@ -0,0 +1,200 @@ +--- +title: 编译 PyTorch 模型 +--- + +# 编译 PyTorch 模型 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/compile_models/from_pytorch.html#sphx-glr-download-how-to-compile-models-from-pytorch-py) 下载完整的示例代码 +::: + +**作者**:[Alex Wong](https://github.com/alexwong/) + +本文介绍了如何用 Relay 部署 PyTorch 模型。 + +首先应安装 PyTorch。此外,还应安装 TorchVision,并将其作为模型合集 (model zoo)。 + +可通过 pip 快速安装: + +``` bash +pip install torch==1.7.0 +pip install torchvision==0.8.1 +``` + +或参考官网:https://pytorch.org/get-started/locally/ + +PyTorch 版本应该和 TorchVision 版本兼容。 + +目前 TVM 支持 PyTorch 1.7 和 1.4,其他版本可能不稳定。 + +``` python +import tvm +from tvm import relay + +import numpy as np + +from tvm.contrib.download import download_testdata + +# 导入 PyTorch +import torch +import torchvision +``` + +## 加载预训练的 PyTorch 模型 + +``` python +model_name = "resnet18" +model = getattr(torchvision.models, model_name)(pretrained=True) +model = model.eval() + +# 通过追踪获取 TorchScripted 模型 +input_shape = [1, 3, 224, 224] +input_data = torch.randn(input_shape) +scripted_model = torch.jit.trace(model, input_data).eval() +``` + +输出结果: + +``` bash +Downloading: "https://download.pytorch.org/models/resnet18-f37072fd.pth" to /workspace/.cache/torch/hub/checkpoints/resnet18-f37072fd.pth + + 0%| | 0.00/44.7M [00:00 英文字符串查找 +node_lookup = tf_testing.NodeLookup(label_lookup_path=map_proto_path, uid_lookup_path=label_path) + +# 打印 TVM 输出的前 5 个预测。 +top_k = predictions.argsort()[-5:][::-1] +for node_id in top_k: + human_string = node_lookup.id_to_string(node_id) + score = predictions[node_id] + print("%s (score = %.5f)" % (human_string, score)) +``` + +输出结果: + +``` bash +African elephant, Loxodonta africana (score = 0.61481) +tusker (score = 0.30387) +Indian elephant, Elephas maximus (score = 0.03343) +banana (score = 0.00023) +rapeseed (score = 0.00021) +``` + +## 在 TensorFlow 上推理 + +在 TensorFlow 上运行对应的模型: + +``` python +def create_graph(): + """从已保存的 GraphDef 文件创建一个计算图,并返回 saver。""" + # 从已保存的 graph_def.pb 创建图形 + with tf_compat_v1.gfile.GFile(model_path, "rb") as f: + graph_def = tf_compat_v1.GraphDef() + graph_def.ParseFromString(f.read()) + graph = tf.import_graph_def(graph_def, name="") + # 调用函数将计算图定义导入默认计算图。 + graph_def = tf_testing.ProcessGraphDefParam(graph_def) + +def run_inference_on_image(image): + """在图像上进行推理。 + + 参数 + ---------- + image: String 类型 + 图像文件名。 + + 返回值 + ------- + 无 + """ + if not tf_compat_v1.gfile.Exists(image): + tf.logging.fatal("File does not exist %s", image) + image_data = tf_compat_v1.gfile.GFile(image, "rb").read() + + # 从已保存的 GraphDef 创建计算图。 + create_graph() + + with tf_compat_v1.Session() as sess: + softmax_tensor = sess.graph.get_tensor_by_name("softmax:0") + predictions = sess.run(softmax_tensor, {"DecodeJpeg/contents:0": image_data}) + + predictions = np.squeeze(predictions) + + # 创建节点 ID --> 英文字符查找 + node_lookup = tf_testing.NodeLookup( + label_lookup_path=map_proto_path, uid_lookup_path=label_path + ) + + # 打印 TensorFlow 的前 5 个预测。 + top_k = predictions.argsort()[-5:][::-1] + print("===== TENSORFLOW RESULTS =======") + for node_id in top_k: + human_string = node_lookup.id_to_string(node_id) + score = predictions[node_id] + print("%s (score = %.5f)" % (human_string, score)) + +run_inference_on_image(img_path) +``` + +输出结果: + +``` bash +===== TENSORFLOW RESULTS ======= +African elephant, Loxodonta africana (score = 0.58394) +tusker (score = 0.33909) +Indian elephant, Elephas maximus (score = 0.03186) +banana (score = 0.00022) +desk (score = 0.00019) +``` + +**脚本总运行时长:** (1 分 6.352 秒) + +[下载 Python 源代码:from_tensorflow.py](https://tvm.apache.org/docs/_downloads/7f1d3d1b878694c201c614c807cdebc8/from_tensorflow.py) + +[下载 Jupyter Notebook:from_tensorflow.ipynb](https://tvm.apache.org/docs/_downloads/83e3b018e8bac8d31bb331d200a33a04/from_tensorflow.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/compile/03-compile_mxnet.md b/versioned_docs/version-0.12.0/how_to/compile/03-compile_mxnet.md new file mode 100644 index 00000000..b317f01f --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/compile/03-compile_mxnet.md @@ -0,0 +1,165 @@ +--- +title: 编译 MXNet 模型 +--- + +# 编译 MXNet 模型 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/compile_models/from_mxnet.html#sphx-glr-download-how-to-compile-models-from-mxnet-py) 下载完整的示例代码 +::: + +**作者**:[Joshua Z. Zhang](https://zhreshold.github.io/),[Kazutaka Morita](https://github.com/kazum) + +本文将介绍如何用 Relay 部署 MXNet 模型。 + +首先安装 mxnet 模块,可通过 pip 快速安装: + +``` bash +pip install mxnet --user +``` + +或参考官方安装指南:https://mxnet.apache.org/versions/master/install/index.html + +``` python +# 一些标准的导包 +import mxnet as mx +import tvm +import tvm.relay as relay +import numpy as np +``` + +## 从 Gluon Model Zoo 下载 Resnet18 模型 + +本节会下载预训练的 imagenet 模型,并对图像进行分类。 + +``` python +from tvm.contrib.download import download_testdata +from mxnet.gluon.model_zoo.vision import get_model +from PIL import Image +from matplotlib import pyplot as plt + +block = get_model("resnet18_v1", pretrained=True) +img_url = "https://github.com/dmlc/mxnet.js/blob/main/data/cat.png?raw=true" +img_name = "cat.png" +synset_url = "".join( + [ + "https://gist.githubusercontent.com/zhreshold/", + "4d0b62f3d01426887599d4f7ede23ee5/raw/", + "596b27d23537e5a1b5751d2b0481ef172f58b539/", + "imagenet1000_clsid_to_human.txt", + ] +) +synset_name = "imagenet1000_clsid_to_human.txt" +img_path = download_testdata(img_url, "cat.png", module="data") +synset_path = download_testdata(synset_url, synset_name, module="data") +with open(synset_path) as f: + synset = eval(f.read()) +image = Image.open(img_path).resize((224, 224)) +plt.imshow(image) +plt.show() + +def transform_image(image): + image = np.array(image) - np.array([123.0, 117.0, 104.0]) + image /= np.array([58.395, 57.12, 57.375]) + image = image.transpose((2, 0, 1)) + image = image[np.newaxis, :] + return image + +x = transform_image(image) +print("x", x.shape) +``` + + ![from mxnet](https://tvm.apache.org/docs/_images/sphx_glr_from_mxnet_001.png) + +输出结果: + +``` bash +Downloading /workspace/.mxnet/models/resnet18_v1-a0666292.zip08d19deb-ddbf-4120-9643-fcfab19e7541 from https://apache-mxnet.s3-accelerate.dualstack.amazonaws.com/gluon/models/resnet18_v1-a0666292.zip... +x (1, 3, 224, 224) +``` + +## 编译计算图 + +只需几行代码,即可将 Gluon 模型移植到可移植计算图上。mxnet.gluon 支持 MXNet 静态图(符号)和 HybridBlock。 + +``` python +shape_dict = {"data": x.shape} +mod, params = relay.frontend.from_mxnet(block, shape_dict) +## 添加 softmax 算子来提高概率 +func = mod["main"] +func = relay.Function(func.params, relay.nn.softmax(func.body), None, func.type_params, func.attrs) +``` + +接下来编译计算图: + +``` python +target = "cuda" +with tvm.transform.PassContext(opt_level=3): + lib = relay.build(func, target, params=params) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +## 在 TVM 上执行可移植计算图 + +接下来用 TVM 重现相同的前向计算: + +``` python +from tvm.contrib import graph_executor + +dev = tvm.cuda(0) +dtype = "float32" +m = graph_executor.GraphModule(lib["default"](dev)) +# 设置输入 +m.set_input("data", tvm.nd.array(x.astype(dtype))) +# 执行 +m.run() +# 得到输出 +tvm_output = m.get_output(0) +top1 = np.argmax(tvm_output.numpy()[0]) +print("TVM prediction top-1:", top1, synset[top1]) +``` + +输出结果: + +``` bash +TVM prediction top-1: 282 tiger cat +``` + +## 使用带有预训练权重的 MXNet 符号 + +MXNet 常用 *arg_params* 和 *aux_params* 分别存储网络参数,下面将展示如何在现有 API 中使用这些权重: + +``` python +def block2symbol(block): + data = mx.sym.Variable("data") + sym = block(data) + args = {} + auxs = {} + for k, v in block.collect_params().items(): + args[k] = mx.nd.array(v.data().asnumpy()) + return sym, args, auxs + +mx_sym, args, auxs = block2symbol(block) +# 通常将其保存/加载为检查点 +mx.model.save_checkpoint("resnet18_v1", 0, mx_sym, args, auxs) +# 磁盘上有 "resnet18_v1-0000.params" 和 "resnet18_v1-symbol.json" +``` + +对于一般性 MXNet 模型: + +``` python +mx_sym, args, auxs = mx.model.load_checkpoint("resnet18_v1", 0) +# 用相同的 API 来获取 Relay 计算图 +mod, relay_params = relay.frontend.from_mxnet(mx_sym, shape_dict, arg_params=args, aux_params=auxs) +# 重复相同的步骤,用 TVM 运行这个模型 +``` + +[下载 Python 源代码:from_mxnet.py](https://tvm.apache.org/docs/_downloads/0e2f38fcb1a1fb3e636e5953aa600dee/from_mxnet.py) + +[下载 Jupyter Notebook:from_mxnet.ipynb](https://tvm.apache.org/docs/_downloads/4bbcfcce3c35b0b795a42c998ceb3770/from_mxnet.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/compile/04-compile_onnx.md b/versioned_docs/version-0.12.0/how_to/compile/04-compile_onnx.md new file mode 100644 index 00000000..4af56dd8 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/compile/04-compile_onnx.md @@ -0,0 +1,139 @@ +--- +title: 编译 ONNX 模型 +--- + +# 编译 ONNX 模型 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/compile_models/from_onnx.html#sphx-glr-download-how-to-compile-models-from-onnx-py) 下载完整的示例代码 +::: + +**作者**:[Joshua Z. Zhang](https://zhreshold.github.io/) + +本文将介绍如何用 Relay 部署 ONNX 模型。 + +首先安装 ONNX 包,最便捷的方法推荐安装 protobuf 编译器: + +``` bash +pip install --user onnx onnxoptimizer +``` + +或参考官方网站:https://github.com/onnx/onnx + +``` python +import onnx +import numpy as np +import tvm +from tvm import te +import tvm.relay as relay +from tvm.contrib.download import download_testdata +``` + +## 加载预训练的 ONNX 模型 + +下面示例中的超分辨率模型与 [ONNX 教程](http://pytorch.org/tutorials/advanced/super_resolution_with_caffe2.html) 中的模型完全相同,跳过 PyTorch 模型的构建部分,下载保存的 ONNX 模型: + +``` python +model_url = "".join( + [ + "https://gist.github.com/zhreshold/", + "bcda4716699ac97ea44f791c24310193/raw/", + "93672b029103648953c4e5ad3ac3aadf346a4cdc/", + "super_resolution_0.2.onnx", + ] +) +model_path = download_testdata(model_url, "super_resolution.onnx", module="onnx") +# 现在磁盘上有 super_resolution.onnx 模型 +onnx_model = onnx.load(model_path) +``` + +## 加载测试图像 + +该模型接收大小为 224x224 的单个图像作为输入,输出沿每个轴放大 3 倍的图像(即大小为 672x672)。为适配输入的 shape,重新缩放猫图像,并转换为 *YCbCr*。然后超分辨率模型应用于亮度(*Y*)通道。 + +``` python +from PIL import Image + +img_url = "https://github.com/dmlc/mxnet.js/blob/main/data/cat.png?raw=true" +img_path = download_testdata(img_url, "cat.png", module="data") +img = Image.open(img_path).resize((224, 224)) +img_ycbcr = img.convert("YCbCr") # convert to YCbCr +img_y, img_cb, img_cr = img_ycbcr.split() +x = np.array(img_y)[np.newaxis, np.newaxis, :, :] +``` + +## 使用 Relay 编译模型 + +通常 ONNX 模型将输入值与参数值混合在一起,输入名称为 *1*,具体要查看模型文档来确定完整的输入和参数名称空间。 + +将 shape 字典传给 *relay.frontend.from_onnx* 方法,以便 Relay 知道哪些 ONNX 参数是输入,哪些是参数,并提供输入尺寸的静态定义: + +``` python +target = "llvm" + +input_name = "1" +shape_dict = {input_name: x.shape} +mod, params = relay.frontend.from_onnx(onnx_model, shape_dict) + +with tvm.transform.PassContext(opt_level=1): + executor = relay.build_module.create_executor( + "graph", mod, tvm.cpu(0), target, params + ).evaluate() +``` + +输出结果: + +``` bash +/workspace/python/tvm/relay/frontend/onnx.py:5785: UserWarning: Mismatched attribute type in ' : kernel_shape' + +==> Context: Bad node spec for node. Name: OpType: Conv + warnings.warn(str(e)) +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +## 在 TVM 上执行 + +``` python +dtype = "float32" +tvm_output = executor(tvm.nd.array(x.astype(dtype))).numpy() +``` + +## 查看结果 + +将输入和输出图像放在一起比对。亮度通道 *Y*是模型的输出。将色度通道 *Cb* 和 *Cr* 调整到匹配简单的双三次算法,然后将图像重新组合,并转换回 *RGB*。 + +``` python +from matplotlib import pyplot as plt + +out_y = Image.fromarray(np.uint8((tvm_output[0, 0]).clip(0, 255)), mode="L") +out_cb = img_cb.resize(out_y.size, Image.BICUBIC) +out_cr = img_cr.resize(out_y.size, Image.BICUBIC) +result = Image.merge("YCbCr", [out_y, out_cb, out_cr]).convert("RGB") +canvas = np.full((672, 672 * 2, 3), 255) +canvas[0:224, 0:224, :] = np.asarray(img) +canvas[:, 672:, :] = np.asarray(result) +plt.imshow(canvas.astype(np.uint8)) +plt.show() +``` + + ![from onnx](https://tvm.apache.org/docs/_images/sphx_glr_from_onnx_001.png) + +输出结果: + +``` bash +/workspace/gallery/how_to/compile_models/from_onnx.py:120: DeprecationWarning: BICUBIC is deprecated and will be removed in Pillow 10 (2023-07-01). Use Resampling.BICUBIC instead. + out_cb = img_cb.resize(out_y.size, Image.BICUBIC) +/workspace/gallery/how_to/compile_models/from_onnx.py:121: DeprecationWarning: BICUBIC is deprecated and will be removed in Pillow 10 (2023-07-01). Use Resampling.BICUBIC instead. + out_cr = img_cr.resize(out_y.size, Image.BICUBIC) +``` + +## 注意 + +ONNX 导入器在导入时默认根据动态 shape 定义模型,编译器在编译时将模型转换为静态 shape。如果失败,模型中可能仍存在动态操作。目前并非所有 TVM 内核都支持动态 shape,如果遇到动态内核错误,请在 discuss.tvm.apache.org 上提交 issue。 + +这个特定的模型是用旧版本的 ONNX 构建的。在导入阶段,ONNX 导入器运行 ONNX 验证程序(可能抛出属性类型不匹配的警告)。由于 TVM 支持许多不同的 ONNX 版本,所以 Relay 模型仍然有效。 + +[下载 Python 源代码:from_onnx.py](https://tvm.apache.org/docs/_downloads/eb551cfff8900ec35fae9f15aa728e45/from_onnx.py) + +[下载 Jupyter Notebook:from_onnx.ipynb](https://tvm.apache.org/docs/_downloads/779f52a44f2b8ab22dc21eee0c27fd4d/from_onnx.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/compile/05-compile_keras.md b/versioned_docs/version-0.12.0/how_to/compile/05-compile_keras.md new file mode 100644 index 00000000..effa5002 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/compile/05-compile_keras.md @@ -0,0 +1,150 @@ +--- +title: 编译 Keras 模型 +--- + +# 编译 Keras 模型 + +:::note +注意:单击 [此处](https://tvm.apache.org/docs/how_to/compile_models/from_keras.html#sphx-glr-download-how-to-compile-models-from-keras-py) 下载完整的示例代码 +::: + +**作者**:[Yuwei Hu](https://huyuwei.github.io/) + +本文介绍如何用 Relay 部署 Keras 模型。 + +首先安装 Keras 和 TensorFlow,可通过 pip 快速安装: + +``` bash +pip install -U keras --user +pip install -U tensorflow --user +``` + +或参考官网:https://keras.io/#installation + +``` python +import tvm +from tvm import te +import tvm.relay as relay +from tvm.contrib.download import download_testdata +import keras +import tensorflow as tf +import numpy as np +``` + +## 加载预训练的 Keras 模型 + +加载 Keras 提供的预训练 resnet-50 分类模型: + +``` python +if tuple(keras.__version__.split(".")) < ("2", "4", "0"): + weights_url = "".join( + [ + "https://github.com/fchollet/deep-learning-models/releases/", + "download/v0.2/resnet50_weights_tf_dim_ordering_tf_kernels.h5", + ] + ) + weights_file = "resnet50_keras_old.h5" +else: + weights_url = "".join( + [ + " https://storage.googleapis.com/tensorflow/keras-applications/", + "resnet/resnet50_weights_tf_dim_ordering_tf_kernels.h5", + ] + ) + weights_file = "resnet50_keras_new.h5" + +weights_path = download_testdata(weights_url, weights_file, module="keras") +keras_resnet50 = tf.keras.applications.resnet50.ResNet50( + include_top=True, weights=None, input_shape=(224, 224, 3), classes=1000 +) +keras_resnet50.load_weights(weights_path) +``` + +## 加载测试图像 + +这里使用的还是先前猫咪的图像: + +``` python +from PIL import Image +from matplotlib import pyplot as plt +from tensorflow.keras.applications.resnet50 import preprocess_input + +img_url = "https://github.com/dmlc/mxnet.js/blob/main/data/cat.png?raw=true" +img_path = download_testdata(img_url, "cat.png", module="data") +img = Image.open(img_path).resize((224, 224)) +plt.imshow(img) +plt.show() +# 预处理输入 +data = np.array(img)[np.newaxis, :].astype("float32") +data = preprocess_input(data).transpose([0, 3, 1, 2]) +print("input_1", data.shape) +``` + + ![图片](https://tvm.apache.org/docs/_images/sphx_glr_from_keras_001.png) + +输出结果: + +``` bash +input_1 (1, 3, 224, 224) +``` + +## 使用 Relay 编译模型 + +将 Keras 模型(NHWC 布局)转换为 Relay 格式(NCHW 布局): + +``` python +shape_dict = {"input_1": data.shape} +mod, params = relay.frontend.from_keras(keras_resnet50, shape_dict) +# 编译模型 +target = "cuda" +dev = tvm.cuda(0) + +# TODO(mbs):opt_level=3 导致 nn.contrib_conv2d_winograd_weight_transform +# 很可能由于潜在的错误,最终出现在 cuda 上的内存验证失败的模块中。 +# 注意:只能在 evaluate() 中传递 context,它不被 create_executor() 捕获。 +with tvm.transform.PassContext(opt_level=0): + model = relay.build_module.create_executor("graph", mod, dev, target, param).evaluate() +``` + +## 在 TVM 上执行 + +``` python +dtype = "float32" +tvm_out = model(tvm.nd.array(data.astype(dtype))) +top1_tvm = np.argmax(tvm_out.numpy()[0]) +``` + +## 查找分类集名称 + +在 1000 个类的分类集中,查找分数最高的第一个: + +``` python +synset_url = "".join( + [ + "https://gist.githubusercontent.com/zhreshold/", + "4d0b62f3d01426887599d4f7ede23ee5/raw/", + "596b27d23537e5a1b5751d2b0481ef172f58b539/", + "imagenet1000_clsid_to_human.txt", + ] +) +synset_name = "imagenet1000_clsid_to_human.txt" +synset_path = download_testdata(synset_url, synset_name, module="data") +with open(synset_path) as f: + synset = eval(f.read()) +print("Relay top-1 id: {}, class name: {}".format(top1_tvm, synset[top1_tvm])) +# 验证 Keras 输出的正确性 +keras_out = keras_resnet50.predict(data.transpose([0, 2, 3, 1])) +top1_keras = np.argmax(keras_out) +print("Keras top-1 id: {}, class name: {}".format(top1_keras, synset[top1_keras])) +``` + +输出结果: + +``` bash +Relay top-1 id: 285, class name: Egyptian cat +Keras top-1 id: 285, class name: Egyptian cat +``` + +[下载 Python 源代码:from_keras.py](https://tvm.apache.org/docs/_downloads/c23f7654585d9b0fa2129e1765b2a8f2/from_keras.py) + +[下载 Jupyter Notebook:from_keras.ipynb](https://tvm.apache.org/docs/_downloads/c82f632d47458e76d2af9821b6778e36/from_keras.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/compile/06-compile_tflite.md b/versioned_docs/version-0.12.0/how_to/compile/06-compile_tflite.md new file mode 100644 index 00000000..e45e1d25 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/compile/06-compile_tflite.md @@ -0,0 +1,209 @@ +--- +title: 编译 TFLite 模型 +--- + +# 编译 TFLite 模型 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/compile_models/from_tflite.html#sphx-glr-download-how-to-compile-models-from-tflite-py) 下载完整的示例代码 +::: + +**作者**:[Zhao Wu](https://github.com/FrozenGene) + +本文介绍如何用 Relay 部署 TFLite 模型。 + +首先安装 TFLite 包。 + +``` bash +# 安装 tflite +pip install tflite==2.1.0 --user +``` + +或者自行生成 TFLite 包,步骤如下: + +``` bash +# 获取 flatc 编译器。 +# 详细可参考 https://github.com/google/flatbuffers,确保正确安装 +flatc --version + +# 获取 TFLite 架构 +wget https://raw.githubusercontent.com/tensorflow/tensorflow/r1.13/tensorflow/lite/schema/schema.fbs + +# 生成 TFLite 包 +flatc --python schema.fbs + +# 将当前文件夹路径(包含生成的 TFLite 模块)添加到 PYTHONPATH。 +export PYTHONPATH=${PYTHONPATH:+$PYTHONPATH:}$(pwd) +``` + +用 `python -c "import tflite"` 命令,检查 TFLite 包是否安装成功。 + +有关如何用 TVM 编译 TFLite 模型的示例如下: + +## 用于下载和提取 zip 文件的程序 + +``` python +import os + +def extract(path): + import tarfile + + if path.endswith("tgz") or path.endswith("gz"): + dir_path = os.path.dirname(path) + tar = tarfile.open(path) + tar.extractall(path=dir_path) + tar.close() + else: + raise RuntimeError("Could not decompress the file: " + path) +``` + +## 加载预训练的 TFLite 模型 + +加载 Google 提供的 mobilenet V1 TFLite 模型: + +``` python +from tvm.contrib.download import download_testdata + +model_url = "http://download.tensorflow.org/models/mobilenet_v1_2018_08_02/mobilenet_v1_1.0_224.tgz" + +# 下载模型 tar 文件,解压得到 mobilenet_v1_1.0_224.tflite +model_path = download_testdata(model_url, "mobilenet_v1_1.0_224.tgz", module=["tf", "official"]) +model_dir = os.path.dirname(model_path) +extract(model_path) + +# 打开 mobilenet_v1_1.0_224.tflite +tflite_model_file = os.path.join(model_dir, "mobilenet_v1_1.0_224.tflite") +tflite_model_buf = open(tflite_model_file, "rb").read() + +# 从缓冲区获取 TFLite 模型 +try: + import tflite + + tflite_model = tflite.Model.GetRootAsModel(tflite_model_buf, 0) +except AttributeError: + import tflite.Model + + tflite_model = tflite.Model.Model.GetRootAsModel(tflite_model_buf, 0) +``` + +## 加载测试图像 + +还是用猫的图像: + +``` python +from PIL import Image +from matplotlib import pyplot as plt +import numpy as np + +image_url = "https://github.com/dmlc/mxnet.js/blob/main/data/cat.png?raw=true" +image_path = download_testdata(image_url, "cat.png", module="data") +resized_image = Image.open(image_path).resize((224, 224)) +plt.imshow(resized_image) +plt.show() +image_data = np.asarray(resized_image).astype("float32") + +# 给图像添加一个维度,形成 NHWC 格式布局 +image_data = np.expand_dims(image_data, axis=0) + +# 预处理图像: +# https://github.com/tensorflow/models/blob/edb6ed22a801665946c63d650ab9a0b23d98e1b1/research/slim/preprocessing/inception_preprocessing.py#L243 +image_data[:, :, :, 0] = 2.0 / 255.0 * image_data[:, :, :, 0] - 1 +image_data[:, :, :, 1] = 2.0 / 255.0 * image_data[:, :, :, 1] - 1 +image_data[:, :, :, 2] = 2.0 / 255.0 * image_data[:, :, :, 2] - 1 +print("input", image_data.shape) +``` + + ![图片](https://tvm.apache.org/docs/_images/sphx_glr_from_tflite_001.png) + +输出结果: + +``` bash +input (1, 224, 224, 3) +``` + +## 使用 Relay 编译模型 + +``` python +# TFLite 输入张量名称、shape 和类型 +input_tensor = "input" +input_shape = (1, 224, 224, 3) +input_dtype = "float32" + +# 解析 TFLite 模型,并将其转换为 Relay 模块 +from tvm import relay, transform + +mod, params = relay.frontend.from_tflite( + tflite_model, shape_dict={input_tensor: input_shape}, dtype_dict={input_tensor: input_dtype} +) + +# 针对 x86 CPU 构建模块 +target = "llvm" +with transform.PassContext(opt_level=3): + lib = relay.build(mod, target, params=params) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +## 在 TVM 上执行 + +``` python +import tvm +from tvm import te +from tvm.contrib import graph_executor as runtime + +# 创建 runtime 执行器模块 +module = runtime.GraphModule(lib["default"](tvm.cpu())) + +# 输入数据 +module.set_input(input_tensor, tvm.nd.array(image_data)) + +# 运行 +module.run() + +# 得到输出 +tvm_output = module.get_output(0).numpy() +``` + +## 显示结果 + +``` python +# 加载标签文件 +label_file_url = "".join( + [ + "https://raw.githubusercontent.com/", + "tensorflow/tensorflow/master/tensorflow/lite/java/demo/", + "app/src/main/assets/", + "labels_mobilenet_quant_v1_224.txt", + ] +) +label_file = "labels_mobilenet_quant_v1_224.txt" +label_path = download_testdata(label_file_url, label_file, module="data") + +# 1001 个类的列表 +with open(label_path) as f: + labels = f.readlines() + +# 将结果转换为一维数据 +predictions = np.squeeze(tvm_output) + +# 获得分数最高的第一个预测值 +prediction = np.argmax(predictions) + +# 将 id 转换为类名,并显示结果 +print("The image prediction result is: id " + str(prediction) + " name: " + labels[prediction]) +``` + +输出结果: + +``` bash +The image prediction result is: id 283 name: tiger cat +``` + +[下载 Python 源代码:from_tflite.py](https://tvm.apache.org/docs/_downloads/a70662bf8dc171d3d17a3945bbbb02e3/from_tflite.py) + +[下载 Jupyter Notebook:from_tflite.ipynb](https://tvm.apache.org/docs/_downloads/23968bb778cd9591b7ad858bf17dcc3e/from_tflite.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/compile/07-compile_coreml.md b/versioned_docs/version-0.12.0/how_to/compile/07-compile_coreml.md new file mode 100644 index 00000000..e6940574 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/compile/07-compile_coreml.md @@ -0,0 +1,128 @@ +--- +title: 编译 CoreML 模型 +--- + +# 编译 CoreML 模型 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/compile_models/from_coreml.html#sphx-glr-download-how-to-compile-models-from-coreml-py) 下载完整的示例代码 +::: + +**作者**:[Joshua Z. Zhang](https://zhreshold.github.io/),[Kazutaka Morita](https://github.com/kazum),[Zhao Wu](https://github.com/FrozenGene) + +本文介绍如何用 Relay 部署 CoreML 模型。 + +首先安装 coremltools 模块,可通过 pip 快速安装: + +``` bash +pip install -U coremltools --user +``` + +或参考官网:https://github.com/apple/coremltools + +``` python +import tvm +from tvm import te +import tvm.relay as relay +from tvm.contrib.download import download_testdata +import coremltools as cm +import numpy as np +from PIL import Image +``` + +## 加载预训练的 CoreML 模型 + +这个例子使用 Apple 提供的预训练的 mobilenet 分类网络。 + +``` python +model_url = "https://docs-assets.developer.apple.com/coreml/models/MobileNet.mlmodel" +model_file = "mobilenet.mlmodel" +model_path = download_testdata(model_url, model_file, module="coreml") +# 现在磁盘上有 mobilenet.mlmodel 模型 +mlmodel = cm.models.MLModel(model_path) +``` + +## 加载测试图像 + +还是用猫的图像: + +``` python +img_url = "https://github.com/dmlc/mxnet.js/blob/main/data/cat.png?raw=true" +img_path = download_testdata(img_url, "cat.png", module="data") +img = Image.open(img_path).resize((224, 224)) +# Mobilenet.mlmodel 的输入是 BGR 格式 +img_bgr = np.array(img)[:, :, ::-1] +x = np.transpose(img_bgr, (2, 0, 1))[np.newaxis, :] +``` + +## 在 Relay 上编译模型 + +现在应该对这个过程较为熟悉了。 + +``` python +target = "llvm" +shape_dict = {"image": x.shape} + +# 解析 CoreML 模型,并转换为 Relay 计算图 +mod, params = relay.frontend.from_coreml(mlmodel, shape_dict) + +with tvm.transform.PassContext(opt_level=3): + lib = relay.build(mod, target, params=params) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +## 在 TVM 上执行 + +这个过程与其他示例的相同。 + +``` python +from tvm.contrib import graph_executor + +dev = tvm.cpu(0) +dtype = "float32" +m = graph_executor.GraphModule(lib["default"](dev)) +# 设置输入 +m.set_input("image", tvm.nd.array(x.astype(dtype))) +# 执行 +m.run() +# 得到输出 +tvm_output = m.get_output(0) +top1 = np.argmax(tvm_output.numpy()[0]) +``` + +## 查找分类集名称 + +在 1000 个类的分类集中,查找分数最高的第一个: + +``` python +synset_url = "".join( + [ + "https://gist.githubusercontent.com/zhreshold/", + "4d0b62f3d01426887599d4f7ede23ee5/raw/", + "596b27d23537e5a1b5751d2b0481ef172f58b539/", + "imagenet1000_clsid_to_human.txt", + ] +) +synset_name = "imagenet1000_clsid_to_human.txt" +synset_path = download_testdata(synset_url, synset_name, module="data") +with open(synset_path) as f: + synset = eval(f.read()) +# 结果应为 Top-1 id 282 class name tiger cat +print("Top-1 id", top1, "class name", synset[top1]) +``` + +输出结果: + +``` bash +Top-1 id 282 class name tiger cat +``` + +[下载 Python 源代码:from_coreml.py](https://tvm.apache.org/docs/_downloads/3aeab7c9d659bf5da70126a1aff7c403/from_coreml.py) + +[下载 Jupyter Notebook:from_coreml.ipynb](https://tvm.apache.org/docs/_downloads/a883b8474634054b6a79c17a288aa8ed/from_coreml.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/compile/08-compile_darknet.md b/versioned_docs/version-0.12.0/how_to/compile/08-compile_darknet.md new file mode 100644 index 00000000..6b1ad760 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/compile/08-compile_darknet.md @@ -0,0 +1,235 @@ +--- +title: 在 DarkNet 模型中编译 YOLO-V2 和 YOLO-V3 +--- + +# 在 DarkNet 模型中编译 YOLO-V2 和 YOLO-V3 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/compile_models/from_darknet.html#sphx-glr-download-how-to-compile-models-from-darknet-py) 下载完整的示例代码 +::: + +**作者**:[Siju Samuel](https://siju-samuel.github.io/) + +本文介绍如何用 TVM 部署 DarkNet 模型。所有必需的模型和库都可通过脚本从 Internet 下载。此脚本运行带有边界框的 YOLO-V2 和 YOLO-V3 模型。DarkNet 解析依赖 CFFI 和 CV2 库,因此执行脚本前要安装这两个库。 + +``` bash +pip install cffi +pip install opencv-python +``` + +``` python +# numpy 和 matplotlib +import numpy as np +import matplotlib.pyplot as plt +import sys + +# tvm 和 relay +import tvm +from tvm import te +from tvm import relay +from ctypes import * +from tvm.contrib.download import download_testdata +from tvm.relay.testing.darknet import __darknetffi__ +import tvm.relay.testing.yolo_detection +import tvm.relay.testing.darknet +``` + +## 选择模型 + +模型有:‘yolov2’、‘yolov3’ 或 ‘yolov3-tiny’ + +``` python +# 模型名称 +MODEL_NAME = "yolov3" +``` + +## 下载所需文件 + +第一次编译的话需要下载 cfg 和 weights 文件。 + +``` python +CFG_NAME = MODEL_NAME + ".cfg" +WEIGHTS_NAME = MODEL_NAME + ".weights" +REPO_URL = "https://github.com/dmlc/web-data/blob/main/darknet/" +CFG_URL = REPO_URL + "cfg/" + CFG_NAME + "?raw=true" +WEIGHTS_URL = "https://pjreddie.com/media/files/" + WEIGHTS_NAME + +cfg_path = download_testdata(CFG_URL, CFG_NAME, module="darknet") +weights_path = download_testdata(WEIGHTS_URL, WEIGHTS_NAME, module="darknet") + +# 下载并加载 DarkNet 库 +if sys.platform in ["linux", "linux2"]: + DARKNET_LIB = "libdarknet2.0.so" + DARKNET_URL = REPO_URL + "lib/" + DARKNET_LIB + "?raw=true" +elif sys.platform == "darwin": + DARKNET_LIB = "libdarknet_mac2.0.so" + DARKNET_URL = REPO_URL + "lib_osx/" + DARKNET_LIB + "?raw=true" +else: + err = "Darknet lib is not supported on {} platform".format(sys.platform) + raise NotImplementedError(err) + +lib_path = download_testdata(DARKNET_URL, DARKNET_LIB, module="darknet") + +DARKNET_LIB = __darknetffi__.dlopen(lib_path) +net = DARKNET_LIB.load_network(cfg_path.encode("utf-8"), weights_path.encode("utf-8"), 0) +dtype = "float32" +batch_size = 1 + +data = np.empty([batch_size, net.c, net.h, net.w], dtype) +shape_dict = {"data": data.shape} +print("Converting darknet to relay functions...") +mod, params = relay.frontend.from_darknet(net, dtype=dtype, shape=data.shape) +``` + +输出结果: + +``` bash +Converting darknet to relay functions... +``` + +## 将计算图导入到 Relay 中 + +编译模型: + +``` python +target = tvm.target.Target("llvm", host="llvm") +dev = tvm.cpu(0) +data = np.empty([batch_size, net.c, net.h, net.w], dtype) +shape = {"data": data.shape} +print("Compiling the model...") +with tvm.transform.PassContext(opt_level=3): + lib = relay.build(mod, target=target, params=params) + +[neth, netw] = shape["data"][2:] # 当前图像 shape 是 608x608 +``` + +输出结果: + +``` bash +Compiling the model... +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +## 加载测试图像 + +``` python +test_image = "dog.jpg" +print("Loading the test image...") +img_url = REPO_URL + "data/" + test_image + "?raw=true" +img_path = download_testdata(img_url, test_image, "data") + +data = tvm.relay.testing.darknet.load_image(img_path, netw, neth) +``` + +输出结果: + +``` bash +Loading the test image... +``` + +## 在 TVM Runtime 上执行 + +这个过程与其他示例的相同。 + +``` python +from tvm.contrib import graph_executor + +m = graph_executor.GraphModule(lib["default"](dev)) + +# 设置输入 +m.set_input("data", tvm.nd.array(data.astype(dtype))) +# 执行 +print("Running the test image...") + +# 检测 +# 阈值 +thresh = 0.5 +nms_thresh = 0.45 + +m.run() +# 得到输出 +tvm_out = [] +if MODEL_NAME == "yolov2": + layer_out = {} + layer_out["type"] = "Region" + # 获取区域层属性(n、out_c、out_h、out_w、classes、coords 和 background) + layer_attr = m.get_output(2).numpy() + layer_out["biases"] = m.get_output(1).numpy() + out_shape = (layer_attr[0], layer_attr[1] // layer_attr[0], layer_attr[2], layer_attr[3]) + layer_out["output"] = m.get_output(0).numpy().reshape(out_shape) + layer_out["classes"] = layer_attr[4] + layer_out["coords"] = layer_attr[5] + layer_out["background"] = layer_attr[6] + tvm_out.append(layer_out) +elif MODEL_NAME == "yolov3": + for i in range(3): + layer_out = {} + layer_out["type"] = "Yolo" + # 获取 yolo 层属性(n、out_c、out_h、out_w、classes 和 total) + layer_attr = m.get_output(i * 4 + 3).numpy() + layer_out["biases"] = m.get_output(i * 4 + 2).numpy() + layer_out["mask"] = m.get_output(i * 4 + 1).numpy() + out_shape = (layer_attr[0], layer_attr[1] // layer_attr[0], layer_attr[2], layer_attr[3]) + layer_out["output"] = m.get_output(i * 4).numpy().reshape(out_shape) + layer_out["classes"] = layer_attr[4] + tvm_out.append(layer_out) +elif MODEL_NAME == "yolov3-tiny": + for i in range(2): + layer_out = {} + layer_out["type"] = "Yolo" + # 获取 yolo 层属性(n、out_c、out_h、out_w、classes 和 total) + layer_attr = m.get_output(i * 4 + 3).numpy() + layer_out["biases"] = m.get_output(i * 4 + 2).numpy() + layer_out["mask"] = m.get_output(i * 4 + 1).numpy() + out_shape = (layer_attr[0], layer_attr[1] // layer_attr[0], layer_attr[2], layer_attr[3]) + layer_out["output"] = m.get_output(i * 4).numpy().reshape(out_shape) + layer_out["classes"] = layer_attr[4] + tvm_out.append(layer_out) + thresh = 0.560 + +# 检测,并画出边界框 +img = tvm.relay.testing.darknet.load_image_color(img_path) +_, im_h, im_w = img.shape +dets = tvm.relay.testing.yolo_detection.fill_network_boxes( + (netw, neth), (im_w, im_h), thresh, 1, tvm_out +) +last_layer = net.layers[net.n - 1] +tvm.relay.testing.yolo_detection.do_nms_sort(dets, last_layer.classes, nms_thresh) + +coco_name = "coco.names" +coco_url = REPO_URL + "data/" + coco_name + "?raw=true" +font_name = "arial.ttf" +font_url = REPO_URL + "data/" + font_name + "?raw=true" +coco_path = download_testdata(coco_url, coco_name, module="data") +font_path = download_testdata(font_url, font_name, module="data") + +with open(coco_path) as f: + content = f.readlines() + +names = [x.strip() for x in content] + +tvm.relay.testing.yolo_detection.show_detections(img, dets, thresh, names, last_layer.classes) +tvm.relay.testing.yolo_detection.draw_detections( + font_path, img, dets, thresh, names, last_layer.classes +) +plt.imshow(img.transpose(1, 2, 0)) +plt.show() +``` + + ![from darknet](https://tvm.apache.org/docs/_images/sphx_glr_from_darknet_001.png) + +输出结果: + +``` bash +Running the test image... +class:['dog 0.994'] left:127 top:227 right:316 bottom:533 +class:['truck 0.9266'] left:471 top:83 right:689 bottom:169 +class:['bicycle 0.9984'] left:111 top:113 right:577 bottom:447 +``` + +**脚本总运行时长:**(1 分 1.020 秒) + +[下载 Python 源代码:from_darknet.py](https://tvm.apache.org/docs/_downloads/7716f96385bd5abb6e822041e285be54/from_darknet.py) + +[下载 Jupyter Notebook:from_darknet.ipynb](https://tvm.apache.org/docs/_downloads/f97d815b408ef3f4d6bcb3e073c2d4dd/from_darknet.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/compile/09-compile_paddlepaddle.md b/versioned_docs/version-0.12.0/how_to/compile/09-compile_paddlepaddle.md new file mode 100644 index 00000000..fe139c25 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/compile/09-compile_paddlepaddle.md @@ -0,0 +1,152 @@ +--- +title: 编译 PaddlePaddle 模型 +--- + +# 编译 PaddlePaddle 模型 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/compile_models/from_paddle.html#sphx-glr-download-how-to-compile-models-from-paddle-py) 下载完整的示例代码 +::: + +**作者**:[Ziyuan Ma](https://github.com/ZiyuanMa/) + +本文介绍如何用 Relay 部署 PaddlePaddle 模型,首先安装 PaddlePaddle(版本>=2.1.3),可通过 pip 快速安装: + +``` bash +pip install paddlepaddle -i https://mirror.baidu.com/pypi/simple +``` + +或参考官方网站:https://www.paddlepaddle.org.cn/install/quick?docurl=/documentation/docs/zh/install/pip/linux-pip.html + +``` python +import tarfile +import paddle +import numpy as np +import tvm +from tvm import relay +from tvm.contrib.download import download_testdata +``` + +输出结果: + +``` bash +/usr/local/lib/python3.7/dist-packages/paddle/vision/transforms/functional_pil.py:36: DeprecationWarning: NEAREST is deprecated and will be removed in Pillow 10 (2023-07-01). Use Resampling.NEAREST or Dither.NONE instead. + 'nearest': Image.NEAREST, +/usr/local/lib/python3.7/dist-packages/paddle/vision/transforms/functional_pil.py:37: DeprecationWarning: BILINEAR is deprecated and will be removed in Pillow 10 (2023-07-01). Use Resampling.BILINEAR instead. + 'bilinear': Image.BILINEAR, +/usr/local/lib/python3.7/dist-packages/paddle/vision/transforms/functional_pil.py:38: DeprecationWarning: BICUBIC is deprecated and will be removed in Pillow 10 (2023-07-01). Use Resampling.BICUBIC instead. + 'bicubic': Image.BICUBIC, +/usr/local/lib/python3.7/dist-packages/paddle/vision/transforms/functional_pil.py:39: DeprecationWarning: BOX is deprecated and will be removed in Pillow 10 (2023-07-01). Use Resampling.BOX instead. + 'box': Image.BOX, +/usr/local/lib/python3.7/dist-packages/paddle/vision/transforms/functional_pil.py:40: DeprecationWarning: LANCZOS is deprecated and will be removed in Pillow 10 (2023-07-01). Use Resampling.LANCZOS instead. + 'lanczos': Image.LANCZOS, +/usr/local/lib/python3.7/dist-packages/paddle/vision/transforms/functional_pil.py:41: DeprecationWarning: HAMMING is deprecated and will be removed in Pillow 10 (2023-07-01). Use Resampling.HAMMING instead. + 'hamming': Image.HAMMING +``` + +## 加载预训练的 ResNet50 模型 + +加载 PaddlePaddle 提供的 ResNet50 预训练模型: + +``` python +url = "https://bj.bcebos.com/x2paddle/models/paddle_resnet50.tar" +model_path = download_testdata(url, "paddle_resnet50.tar", module="model") + +with tarfile.open(model_path) as tar: + names = tar.getnames() + for name in names: + tar.extract(name, "./") + +model = paddle.jit.load("./paddle_resnet50/model") +``` + +输出结果: + +``` bash +/usr/local/lib/python3.7/dist-packages/paddle/fluid/backward.py:1666: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3,and in 3.9 it will stop working + return list(x) if isinstance(x, collections.Sequence) else [x] +``` + +## 加载测试图像 + +还是用猫的图像: + +``` python +from PIL import Image +import paddle.vision.transforms as T + +transforms = T.Compose( + [ + T.Resize((256, 256)), + T.CenterCrop(224), + T.ToTensor(), + T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), + ] +) + +img_url = "https://github.com/dmlc/mxnet.js/blob/main/data/cat.png?raw=true" +img_path = download_testdata(img_url, "cat.png", module="data") +img = Image.open(img_path).resize((224, 224)) + +img = transforms(img) +img = np.expand_dims(img, axis=0) +``` + +## 使用 Relay 编译模型 + +``` python +target = "llvm" +shape_dict = {"inputs": img.shape} +mod, params = relay.frontend.from_paddle(model, shape_dict) + +with tvm.transform.PassContext(opt_level=3): + executor = relay.build_module.create_executor( + "graph", mod, tvm.cpu(0), target, params + ).evaluate() +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +## 在 TVM 上执行 + +``` python +dtype = "float32" +tvm_output = executor(tvm.nd.array(img.astype(dtype))).numpy() +``` + +## 查找分类集名称 + +在 1000 个类的分类集中,查找分数最高的第一个: + +``` python +synset_url = "".join( + [ + "https://gist.githubusercontent.com/zhreshold/", + "4d0b62f3d01426887599d4f7ede23ee5/raw/", + "596b27d23537e5a1b5751d2b0481ef172f58b539/", + "imagenet1000_clsid_to_human.txt", + ] +) +synset_name = "imagenet1000_clsid_to_human.txt" +synset_path = download_testdata(synset_url, synset_name, module="data") +with open(synset_path) as f: + synset = f.readlines() + +top1 = np.argmax(tvm_output[0]) +print(f"TVM prediction top-1 id: {top1}, class name: {synset[top1]}") +``` + +输出结果: + +``` bash +TVM prediction top-1 id: 282, class name: 282: 'tiger cat', +``` + +[下载 Python 源代码:from_paddle.py](https://tvm.apache.org/docs/_downloads/16269b77359771348d507395692524cf/from_paddle.py) + +[下载 Jupyter Notebook:from_paddle.ipynb](https://tvm.apache.org/docs/_downloads/a608d8b69371e9bc149dd89f6db2c38e/from_paddle.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/compile/10-compile_oneflow.md b/versioned_docs/version-0.12.0/how_to/compile/10-compile_oneflow.md new file mode 100644 index 00000000..135b4e8e --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/compile/10-compile_oneflow.md @@ -0,0 +1,240 @@ +--- +title: 编译 OneFlow 模型 +--- + +# 编译 OneFlow 模型 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/compile_models/from_oneflow.html#sphx-glr-download-how-to-compile-models-from-oneflow-py) 下载完整的示例代码 +::: + +**作者**:[Xiaoyu Zhang](https://github.com/BBuf/) + +本文介绍如何用 Relay 部署 OneFlow 模型。 + +首先安装 OneFlow 包,可通过 pip 快速安装: + +``` bash +pip install flowvision==0.1.0 +python3 -m pip install -f https://release.oneflow.info oneflow==0.7.0+cpu +``` + +或参考官网:https://github.com/Oneflow-Inc/oneflow + +目前 TVM 支持 OneFlow 0.7.0,其他版本可能不稳定。 + +``` python +import os, math +from matplotlib import pyplot as plt +import numpy as np +from PIL import Image + +# OneFlow 导入 +import flowvision +import oneflow as flow +import oneflow.nn as nn + +import tvm +from tvm import relay +from tvm.contrib.download import download_testdata +``` + +输出结果: + +``` bash +/usr/local/lib/python3.7/dist-packages/flowvision/transforms/functional_pil.py:193: DeprecationWarning: BILINEAR is deprecated and will be removed in Pillow 10 (2023-07-01). Use Resampling.BILINEAR instead. + def resize(img, size, interpolation=Image.BILINEAR): +/usr/local/lib/python3.7/dist-packages/flowvision/transforms/functional.py:65: DeprecationWarning: NEAREST is deprecated and will be removed in Pillow 10 (2023-07-01). Use Resampling.NEAREST or Dither.NONE instead. + Image.NEAREST: "nearest", +/usr/local/lib/python3.7/dist-packages/flowvision/transforms/functional.py:66: DeprecationWarning: BILINEAR is deprecated and will be removed in Pillow 10 (2023-07-01). Use Resampling.BILINEAR instead. + Image.BILINEAR: "bilinear", +/usr/local/lib/python3.7/dist-packages/flowvision/transforms/functional.py:67: DeprecationWarning: BICUBIC is deprecated and will be removed in Pillow 10 (2023-07-01). Use Resampling.BICUBIC instead. + Image.BICUBIC: "bicubic", +/usr/local/lib/python3.7/dist-packages/flowvision/transforms/functional.py:68: DeprecationWarning: BOX is deprecated and will be removed in Pillow 10 (2023-07-01). Use Resampling.BOX instead. + Image.BOX: "box", +/usr/local/lib/python3.7/dist-packages/flowvision/transforms/functional.py:69: DeprecationWarning: HAMMING is deprecated and will be removed in Pillow 10 (2023-07-01). Use Resampling.HAMMING instead. + Image.HAMMING: "hamming", +/usr/local/lib/python3.7/dist-packages/flowvision/transforms/functional.py:70: DeprecationWarning: LANCZOS is deprecated and will be removed in Pillow 10 (2023-07-01). Use Resampling.LANCZOS instead. + Image.LANCZOS: "lanczos", +/usr/local/lib/python3.7/dist-packages/flowvision/data/auto_augment.py:28: DeprecationWarning: BILINEAR is deprecated and will be removed in Pillow 10 (2023-07-01). Use Resampling.BILINEAR instead. + _RANDOM_INTERPOLATION = (Image.BILINEAR, Image.BICUBIC) +/usr/local/lib/python3.7/dist-packages/flowvision/data/auto_augment.py:28: DeprecationWarning: BICUBIC is deprecated and will be removed in Pillow 10 (2023-07-01). Use Resampling.BICUBIC instead. + _RANDOM_INTERPOLATION = (Image.BILINEAR, Image.BICUBIC) +``` + +## 加载和保存 OneFlow 的预训练模型 + +``` python +model_name = "resnet18" +model = getattr(flowvision.models, model_name)(pretrained=True) +model = model.eval() + +model_dir = "resnet18_model" +if not os.path.exists(model_dir): + flow.save(model.state_dict(), model_dir) +``` + +输出结果: + +``` bash +Downloading: "https://oneflow-public.oss-cn-beijing.aliyuncs.com/model_zoo/flowvision/classification/ResNet/resnet18.zip" to /workspace/.oneflow/flowvision_cache/resnet18.zip + + 0%| | 0.00/41.5M [00:00 +(1, 3, 224, 224) +``` + +## 查找分类集名称 + +在 1000 个类的分类集中,查找分数最高的第一个: + +``` python +synset_url = "".join( + [ + "https://raw.githubusercontent.com/Cadene/", + "pretrained-models.pytorch/master/data/", + "imagenet_synsets.txt", + ] +) +synset_name = "imagenet_synsets.txt" +synset_path = download_testdata(synset_url, synset_name, module="data") +with open(synset_path) as f: + synsets = f.readlines() + +synsets = [x.strip() for x in synsets] +splits = [line.split(" ") for line in synsets] +key_to_classname = {spl[0]: " ".join(spl[1:]) for spl in splits} + +class_url = "".join( + [ + "https://raw.githubusercontent.com/Cadene/", + "pretrained-models.pytorch/master/data/", + "imagenet_classes.txt", + ] +) +class_name = "imagenet_classes.txt" +class_path = download_testdata(class_url, class_name, module="data") +with open(class_path) as f: + class_id_to_key = f.readlines() + +class_id_to_key = [x.strip() for x in class_id_to_key] + +# 获得 TVM 分数最高的第一个结果 +top1_tvm = np.argmax(tvm_output.numpy()[0]) +tvm_class_key = class_id_to_key[top1_tvm] + +# 将输入转换为 OneFlow 变量,并获取 OneFlow 结果进行比较 +with flow.no_grad(): + torch_img = flow.from_numpy(img) + output = model(torch_img) + + # 获取 OneFlow 分数最高的第一个结果 + top_oneflow = np.argmax(output.numpy()) + oneflow_class_key = class_id_to_key[top_oneflow] + +print("Relay top-1 id: {}, class name: {}".format(top1_tvm, key_to_classname[tvm_class_key])) +print( + "OneFlow top-1 id: {}, class name: {}".format(top_oneflow, key_to_classname[oneflow_class_key]) +) +``` + +输出结果: + +``` bash +Relay top-1 id: 281, class name: tabby, tabby cat +OneFlow top-1 id: 281, class name: tabby, tabby cat +``` + +[下载 Python 源代码:from_oneflow.py](https://tvm.apache.org/docs/_downloads/f7ae979fbe61064749ce0fb7a621eb4c/from_oneflow.py) + +[下载 Jupyter Notebook:from_oneflow.ipynb](https://tvm.apache.org/docs/_downloads/2e7b51cb39c472626dd3f046d9b89966/from_oneflow.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/compile/_category_.json b/versioned_docs/version-0.12.0/how_to/compile/_category_.json new file mode 100644 index 00000000..346153ae --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/compile/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 110 +} diff --git a/versioned_docs/version-0.12.0/how_to/compile/index.md b/versioned_docs/version-0.12.0/how_to/compile/index.md new file mode 100644 index 00000000..ab019448 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/compile/index.md @@ -0,0 +1,17 @@ +--- +title: 编译深度学习模型 +--- + +TVM 包括多种前端,它们可以导入不同格式的模型。这些操作指南演示了如何用 Python API 导入模型: + +* [编译 PyTorch 模型](compile_pytorch) +* [编译 TensorFlow 模型](compile_tensorflow) +* [编译 MXNet 模型](compile_mxnet) +* [编译 ONNX 模型](compile_onnx) +* [编译 Keras 模型](compile_keras) +* [编译 TFLite 模型](compile_tflite) +* [编译 CoreML 模型](compile_coreml) +* [编译 DarkNet 中的 YOLO-V2 和 YOLO-V3 模型](compile_darknet) +* [编译 PaddlePaddle 模型](compile_paddlepaddle) +* [编译 OneFlow Models](compile_oneflow) + diff --git a/versioned_docs/version-0.12.0/how_to/deploy/01-deploy_c++.md b/versioned_docs/version-0.12.0/how_to/deploy/01-deploy_c++.md new file mode 100644 index 00000000..643e65eb --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/deploy/01-deploy_c++.md @@ -0,0 +1,25 @@ +# 使用 C++ API 部署 TVM 模块 + +[apps/howto_deploy](https://github.com/apache/tvm/tree/main/apps/howto_deploy) 中给出了部署 TVM 模块的示例,执行下面的命令运行该示例: + +``` bash +cd apps/howto_deploy +./run_example.sh +``` + +## 获取 TVM Runtime 库 + +唯一要做的是链接到 target 平台中的 TVM runtime。 TVM 给出了一个最小 runtime,它的开销大约在 300K 到 600K 之间,具体值取决于使用模块的数量。大多数情况下,可用 `libtvm_runtime.so` 文件去构建。 + +若构建 `libtvm_runtime` 有困难,可查看 [tvm_runtime_pack.cc](https://github.com/apache/tvm/tree/main/apps/howto_deploy/tvm_runtime_pack.cc)(集成了 TVM runtime 的所有示例)。用构建系统来编译这个文件,然后将它包含到项目中。 + +查看 [apps](https://github.com/apache/tvm/tree/main/apps/) 获取在 iOS、Android 和其他平台上,用 TVM 构建的应用示例。 + +## 动态库 vs. 系统模块 + +TVM 有两种使用编译库的方法,查看 [prepare_test_libs.py](https://github.com/apache/tvm/tree/main/apps/howto_deploy/prepare_test_libs.py) 了解如何生成库,查看 [cpp_deploy.cc](https://github.com/apache/tvm/tree/main/apps/howto_deploy/cpp_deploy.cc) 了解如何使用它们。 + +* 把库存储为共享库,并动态加载到项目中。 +* 将编译好的库以系统模块模式绑定到项目中。 + +动态加载更加灵活,能快速加载新模块。系统模块是一种更 `static` 的方法,可用在动态库加载不可用的地方。 \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/deploy/02-deploy_android.md b/versioned_docs/version-0.12.0/how_to/deploy/02-deploy_android.md new file mode 100644 index 00000000..9e33010f --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/deploy/02-deploy_android.md @@ -0,0 +1,19 @@ +# 部署到 Android + +## 为 Android Target 构建模型 + +针对 Android target 的 Relay 模型编译遵循和 android_rpc 相同的方法,以下代码会保存 Android target 所需的编译输出: + +``` python +lib.export_library("deploy_lib.so", ndk.create_shared) +with open("deploy_graph.json", "w") as fo: + fo.write(graph.json()) +with open("deploy_param.params", "wb") as fo: + fo.write(runtime.save_param_dict(params)) +``` + +deploy_lib.so、deploy_graph.json、deploy_param.params 将转到 Android target。 + +## 适用于 Android Target 的 TVM Runtime + +参考 [此处](https://github.com/apache/tvm/blob/main/apps/android_deploy/README.md#build-and-installation) 为 Android target 构建 CPU/OpenCL 版本的 TVM runtime。参考这个 [Java](https://github.com/apache/tvm/blob/main/apps/android_deploy/app/src/main/java/org/apache/tvm/android/demo/MainActivity.java) 示例来了解 Android Java TVM API,以及如何加载和执行模型。 \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/deploy/03-integrate.md b/versioned_docs/version-0.12.0/how_to/deploy/03-integrate.md new file mode 100644 index 00000000..d3cff81f --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/deploy/03-integrate.md @@ -0,0 +1,38 @@ +# 将 TVM 集成到项目中 + +TVM runtime 具有轻量级和可移植性的特点,有几种方法可将 TVM 集成到项目中。 + +本文介绍如何将 TVM 作为 JIT 编译器集成到项目中,从而用它在系统上生成函数的方法 + +## DLPack 支持 + +TVM 的生成函数遵循 PackedFunc 约定,它是一个可以接受位置参数(包括标准类型,如浮点、整数、字符串)的函数。PackedFunc 采用 [DLPack](https://github.com/dmlc/dlpack) 约定中的 DLTensor 指针。唯一要做的是创建一个对应的 DLTensor 对象。 + +## 集成用户自定义的 C++ 数组 + +在 C++ 中唯一要做的就是将你的数组转换为 DLTensor,并将其地址作为 `DLTensor*` 传递给生成的函数。 + +## 集成用户自定义的 Python 数组 + +针对 Python 对象 `MyArray`,需要做: + +* 将 `_tvm_tcode` 字段添加到返回 `tvm.TypeCode.ARRAY_HANDLE` 的数组中 +* 在对象中支持 `_tvm_handle` 属性(以 Python 整数形式返回 DLTensor 的地址) +* 用 `tvm.register_extension` 注册这个类 + +``` python +# 示例代码 +import tvm + +class MyArray(object): + _tvm_tcode = tvm.TypeCode.ARRAY_HANDLE + + @property + def _tvm_handle(self): + dltensor_addr = self.get_dltensor_addr() + return dltensor_addr + +# 将注册的步骤放在单独的文件 mypkg.tvm.py 中 +# 根据需要选择性地导入依赖 +tvm.register_extension(MyArray) +``` \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/deploy/04-hls.md b/versioned_docs/version-0.12.0/how_to/deploy/04-hls.md new file mode 100644 index 00000000..f690dfb9 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/deploy/04-hls.md @@ -0,0 +1,157 @@ +# HLS 后端示例 + +TVM 支持带有 SDAccel 的 Xilinx FPGA 板,接下来介绍如何将 TVM 部署到 AWS F1 FPGA 实例。 + +:::note +此功能仍处于测试阶段,目前无法用 SDAccel 部署端到端神经网络。 +::: + +本教程使用了两个 Python 脚本: + +* build.py - 用于合成 FPGA 比特流的脚本。 + + ``` python + import tvm + from tvm import te + + tgt= tvm.target.Target("sdaccel", host="llvm") + + n = te.var("n") + A = te.placeholder((n,), name='A') + B = te.placeholder((n,), name='B') + C = te.compute(A.shape, lambda i: A[i] + B[i], name="C") + + s = te.create_schedule(C.op) + px, x = s[C].split(C.op.axis[0], nparts=1) + + s[C].bind(px, tvm.te.thread_axis("pipeline")) + + fadd = tvm.build(s, [A, B, C], tgt, name="myadd") + fadd.save("myadd.o") + fadd.imported_modules[0].save("myadd.xclbin") + + tvm.contrib.cc.create_shared("myadd.so", ["myadd.o"]) + ``` + +* run.py - 将 FPGA 作为加速器的脚本。 + + ``` python + import tvm + import numpy as np + import os + + tgt = "sdaccel" + + fadd = tvm.runtime.load_module("myadd.so") + if os.environ.get("XCL_EMULATION_MODE"): + fadd_dev = tvm.runtime.load_module("myadd.xclbin") + else: + fadd_dev = tvm.runtime.load_module("myadd.awsxclbin") + fadd.import_module(fadd_dev) + + dev = tvm.device(tgt, 0) + + n = 1024 + a = tvm.nd.array(np.random.uniform(size=n).astype("float32"), dev) + b = tvm.nd.array(np.random.uniform(size=n).astype("float32"), dev) + c = tvm.nd.array(np.zeros(n, dtype="float32"), dev) + + fadd(a, b, c) + tvm.testing.assert_allclose(c.numpy(), a.numpy() + b.numpy()) + ``` + +## 设置 + +* 用 FPGA Developer AMI 启动实例。无需 F1 实例来进行仿真和合成,因此推荐用开销较低的实例。 +* 设置 AWS FPGA 开发套件: + + ``` bash + git clone https://github.com/aws/aws-fpga.git + cd aws-fpga + source sdaccel_setup.sh + source ${XILINX_SDX}/settings64.sh + ``` + +* 启用 OpenCL 前设置 TVM。 + +## 仿真 + +* 为仿真创建 emconfig.json: + + ``` bash + emconfigutil --platform ${AWS_PLATFORM} --nd 1 + ``` + +* 将 emconfig.json 复制到 Python binary 目录下:因为当前的 Xilinx 工具包假定宿主机的二进制文件和 emconfig.json 文件处于同一路径。 + + ``` bash + cp emconfig.json $(dirname $(which python)) + ``` + +* 运行软件仿真: + + ``` bash + export XCL_EMULATION_MODE=1 + export XCL_TARGET=sw_emu + + python build.py + python run.py + ``` + +* 运行硬件仿真: + + ``` bash + export XCL_EMULATION_MODE=1 + export XCL_TARGET=hw_emu + + python build.py + python run.py + ``` + +## 合成 + +* 用以下脚本进行合成: + + ``` bash + unset XCL_EMULATION_MODE + export XCL_TARGET=hw + + python build.py + ``` + +* 创建 AWS FPGA 镜像,并将其上传到 AWS S3: + + ``` bash + ${SDACCEL_DIR}/tools/create_sdaccel_afi.sh \ + -xclbin=myadd.xclbin -o=myadd \ + -s3_bucket= -s3_dcp_key= \ + -s3_logs_key= + ``` + +这会生成 awsxclbin 文件(在 F1 实例上使用 AWS FPGA 镜像必需)。 + +## 运行 + +* 启动 Amazon EC2 F1 实例。 +* 将 `myadd.so`,`myadd.awsxclbin` 和 `run.py` 复制到 F1 实例中。 +* 设置 AWS FPGA 开发套件: + + ``` bash + git clone https://github.com/aws/aws-fpga.git + cd aws-fpga + source sdaccel_setup.sh + ``` + +* 启用 OpenCL 前设置 TVM。 +* 以 root 身份设置环境变量: + + ``` bash + sudo sh + source ${INSTALL_ROOT}/setup.sh + ``` + +* 运行: + + ``` bash + python run.py + ``` \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/deploy/05-relay_arm.md b/versioned_docs/version-0.12.0/how_to/deploy/05-relay_arm.md new file mode 100644 index 00000000..9e066f71 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/deploy/05-relay_arm.md @@ -0,0 +1,158 @@ +# Relay Arm® 计算库集成 + +**作者**:[Luke Hutton](https://github.com/lhutton1) + +## 介绍 + +Arm 计算库(ACL)是一个开源项目,它为 Arm CPU 和 GPU 提供了加速内核。目前,集成将算子迁移到 ACL 以在库中使用手工制作的汇编程序例程。通过将选择算子从 Relay 计算图迁移到 ACL,可在此类设备上实现性能提升。 + +## 安装 Arm 计算库 + +安装 Arm 计算库前,了解要构建的架构非常重要。一种方法是使用 *lscpu*,并查找 CPU 的“模型名称”,然后,可以使用它通过在线查看来确定架构。 + +TVM 目前只支持 v21.08 版本的 ACL,构建和安装所需的库的推荐方法如下: + +* 使用位于 *docker/install/ubuntu_download_arm_compute_lib_binaries.sh* 的脚本,为 *target_lib* 指定的架构和扩展下载 ACL 二进制文件,它们将安装到 *install_path* 表示的位置。 +* 或从 https://github.com/ARM-software/ComputeLibrary/releases 下载预构建的二进制文件。 使用此包时,要为所需的架构和扩展选择二进制文件,并确保它们对 CMake 可见: + + ``` bash + cd /lib + mv .//* . + ``` + +这两种情况都要将 USE_ARM_COMPUTE_LIB_GRAPH_EXECUTOR 设置为 ACL 包所在的路径。 CMake 会在 /path-to-acl/,/path-to-acl/lib 和 /path-to-acl/build 中查找所需的二进制文件。如何使用这些配置选项,请参阅下一小节。 + +## 使用 ACL support 构建 + +当前的实现在 CMake 中有两个单独的构建选项。这种拆分的原因是 ACL 不能在 x86 机器上使用。但是,我们仍希望在 x86 机器上编译 ACL runtime 模块。 + +* USE_ARM_COMPUTE_LIB=ON/OFF - 启用此标志能添加对编译 ACL runtime 模块的支持。 +* USE_ARM_COMPUTE_LIB_GRAPH_EXECUTOR=ON/OFF/path-to-acl - 启用此标志将允许图执行器计算 ACL 迁移的函数。 + +这些标志可根据你的设置应用于不同的场景。例如,若要在 x86 机器上编译 ACL 模块,并通过 RPC 在远程 Arm 设备上运行,则需要在 x86 机器上设置 USE_ARM_COMPUTE_LIB=ON,在远程 AArch64 设备上设置 USE_ARM_COMPUTE_LIB_GRAPH_EXECUTOR=ON。 + +默认这两个选项都设置为 OFF。设置 USE_ARM_COMPUTE_LIB_GRAPH_EXECUTOR=ON 意味着 CMake 会在默认位置和 /path-to-tvm-project/acl/ 目录下搜索 ACL 二进制文件(参阅 [https://cmake.org/cmake/help/v3.4/command/find_library.html](https://cmake.org/cmake/help/v3.4/command/find_library.html))。若要设置搜索 ACL 的路径,可在 ON 的位置指定。 + +这些标志应在 config.cmake 文件中进行设置,如: + +``` cmake +set(USE_ARM_COMPUTE_LIB ON) +set(USE_ARM_COMPUTE_LIB_GRAPH_EXECUTOR /path/to/acl) +``` + +## 使用 + +:::note +此部分可能与 API 的更改不同步。 +::: + +创建一个 Relay 计算图(单个算子或整个计算图),使得任何 Relay 计算图都可以作为输入。ACL 集成只会选择支持的算子进行迁移,而其他的将通过 TVM 计算。(本例用的是单个 max_pool2d 算子)。 + +``` python +import tvm +from tvm import relay + +data_type = "float32" +data_shape = (1, 14, 14, 512) +strides = (2, 2) +padding = (0, 0, 0, 0) +pool_size = (2, 2) +layout = "NHWC" +output_shape = (1, 7, 7, 512) + +data = relay.var('data', shape=data_shape, dtype=data_type) +out = relay.nn.max_pool2d(data, pool_size=pool_size, strides=strides, layout=layout, padding=padding) +module = tvm.IRModule.from_expr(out) +``` + +为 ACL 的计算图进行注释和分区: + +``` python +from tvm.relay.op.contrib.arm_compute_lib import partition_for_arm_compute_lib +module = partition_for_arm_compute_lib(module) +``` + +构建 Relay 计算图: + +``` python +target = "llvm -mtriple=aarch64-linux-gnu -mattr=+neon" +with tvm.transform.PassContext(opt_level=3, disabled_pass=["AlterOpLayout"]): + lib = relay.build(module, target=target) +``` + +导出模块: + +``` bash +lib_path = '~/lib_acl.so' +cross_compile = 'aarch64-linux-gnu-c++' +lib.export_library(lib_path, cc=cross_compile) +``` + +必须在 Arm 设备上运行推理。若在 x86 设备上编译,在 AArch64 上运行,则需要借助 RPC 机制(参考 [RPC 机制的使用教程](/docs/tutorial/rpc))。 + +``` python +dev = tvm.cpu(0) +loaded_lib = tvm.runtime.load_module('lib_acl.so') +gen_module = tvm.contrib.graph_executor.GraphModule(loaded_lib['default'](dev)) +d_data = np.random.uniform(0, 1, data_shape).astype(data_type) +map_inputs = {'data': d_data} +gen_module.set_input(**map_inputs) +gen_module.run() +``` + +## 更多示例 + +以上示例仅展示了如何用 ACL 迁移单个 Maxpool2D 的基本示例。若要查看所有实现的算子和网络的更多示例,参阅 tests:*tests/python/contrib/test_arm_compute_lib*(可修改 *test_config.json* 来配置如何在 *infrastructure.py* 中创建远程设备,从而配置 runtime 测试的运行方式。 + +以 *test_config.json* 的配置为例: + +* connection_type - RPC 连接的类型。选项:local、tracker 和 remote。 +* host - 要连接的主机设备。 +* port - 连接时使用的端口。 +* target - 用于编译的 target。 +* device_key - 通过 tracker 连接时的设备密钥。 +* cross_compile - 连接非 arm 平台时交叉编译器的路径,例如 aarch64-linux-gnu-g++。 + +``` json +{ + "connection_type": "local", + "host": "127.0.0.1", + "port": 9090, + "target": "llvm -mtriple=aarch64-linux-gnu -mattr=+neon", + "device_key": "", + "cross_compile": "" +} +``` + +## 支持的算子 + +| Relay 节点 | 备注 | +|:---|:---| +| nn.conv2d | **fp32:**
Simple: nn.conv2d Composite: nn.pad?, nn.conv2d, nn.bias_add?, nn.relu?
支持深度和普通卷积(内核为 3x3 或 5x5 且步幅为 1x1 或 2x2 时),不支持分组卷积。``` | +| qnn.conv2d | **uint8:**
Composite: nn.pad?, nn.conv2d, nn.bias_add?, nn.relu?, qnn.requantizeNormal
支持深度和普通卷积(当内核为 3x3 或 5x5,步长为 1x1 或 2x2 时),不支持分组卷积。 | +| nn.dense | **fp32:**
Simple: nn.dense Composite: nn.dense, nn.bias_add? | +| qnn.dense | **uint8:**
Composite: qnn.dense, nn.bias_add?, qnn.requantize | +| nn.max_pool2d | fp32, uint8 | +| nn.global_max_pool2d | fp32, uint8 | +| nn.avg_pool2d | **fp32:**
Simple: nn.avg_pool2d
**uint8:**
Composite: cast(int32), nn.avg_pool2d, cast(uint8) | +| nn.global_avg_pool2d | **fp32:**
Simple: nn.global_avg_pool2d
**uint8:**
Composite: cast(int32), nn.avg_pool2d, cast(uint8) | +| power(of 2) + nn.avg_pool2d + sqrt | L2 池化的一种特殊情况。
**fp32:**
Composite: power(of 2), nn.avg_pool2d, sqrt | +| reshape | fp32, uint8 | +| maximum | fp32 | +| add | fp32 | +| qnn.add | uint8 | + +:::note +复合算子由映射到单个 Arm 计算库的算子组成。从 Arm 计算库的角度来看,可以将其视为单个融合算子。“?”是构成复合算子的一系列算子中的可选算子。 +::: + +## 添加新算子 + +添加新算子需要修改多处,本节将分享需要修改的内容和位置,但不会深入探讨单个算子的复杂性(这个问题留给开发者思考)。 + +下面是要修改的几个文件: + +* python/relay/op/contrib/arm_compute_lib.py:定义了要用 op.register 装饰器迁移的算子——意味着注释 pass 认为此算子可迁移 ACL。 +* src/relay/backend/contrib/arm_compute_lib/codegen.cc:实现 Create[OpName]JSONNode 的方法;声明算子如何由 JSON 表示,可用来创建 ACL 模块。 +* src/runtime/contrib/arm_compute_lib/acl_runtime.cc:实现 Create[OpName]Layer 方法;定义如何用 JSON 表示来创建 ACL 函数;只定义了如何将 JSON 表示转换为 ACL API。 +* tests/python/contrib/test_arm_compute_lib:为给定的算子添加单元测试。 diff --git a/versioned_docs/version-0.12.0/how_to/deploy/06-relay_tensorrt.md b/versioned_docs/version-0.12.0/how_to/deploy/06-relay_tensorrt.md new file mode 100644 index 00000000..ff96a1c9 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/deploy/06-relay_tensorrt.md @@ -0,0 +1,169 @@ +# Relay TensorRT 集成 + +**作者**:[Trevor Morris](https://github.com/trevor-m) + +## 介绍 + +NVIDIA TensorRT 是一个用于优化深度学习推理的库。这种集成尽可能多地将算子从 Relay 迁移到 TensorRT,无需对 schedule 调优,即可提升 NVIDIA GPU 的性能。 + +本教程演示如何安装 TensorRT 以及如何构建 TVM,来启用 TensorRT BYOC 和 runtime。此外,还给出了示例代码,演示了如何用 TensorRT 编译和运行 ResNet-18 模型,以及如何配置编译和 runtime 设置。最后,还记录了支持的算子,以及如何扩展集成来支持其他算子。 + +## 安装 TensorRT + +若要下载 TensorRT,需要创建一个 NVIDIA 开发者帐户,可参考 NVIDIA 的文档来了解更多信息:[https://docs.nvidia.com/deeplearning/tensorrt/install-guide/index.html](https://docs.nvidia.com/deeplearning/tensorrt/install-guide/index.html)。若有 Jetson 设备(如 TX1、TX2、Xavier 或 Nano),则 TensorRT 可能已由 JetPack SDK 安装到设备了 。 + +安装 TensorRT 的两种方法: + +* 通过 deb 或 rpm 包系统安装。 +* 通过 tar 文件安装。 + +用 tar 文件的安装方法,必须将解压后的 tar 路径传到 USE_TENSORRT_RUNTIME=/path/to/TensorRT 中;用系统安装的方法,USE_TENSORRT_RUNTIME=ON 会自动定位安装路径。 + +## 使用 TensorRT 支持构建 TVM + +TVM 的 TensorRT 集成有两个单独的构建标志,这些标志启用了交叉编译:USE_TENSORRT_CODEGEN=ON —— 在支持 TensorRT 的主机上构建模块; USE_TENSORRT_RUNTIME=ON —— 使得边界设备上的 TVM runtime 能够执行 TensorRT 模块。若要编译并执行具有相同 TVM 构建的模型,则应启用这两者。 + +* USE_TENSORRT_CODEGEN=ON/OFF - 使得无需任何 TensorRT 库即可编译 TensorRT 模块。 +* USE_TENSORRT_RUNTIME=ON/OFF/path-to-TensorRT - 启用 TensorRT runtime 模块,它用已安装的 TensorRT 库来构建 TVM。 + +config.cmake 文件中的设置示例: + +``` cmake +set(USE_TENSORRT_CODEGEN ON) +set(USE_TENSORRT_RUNTIME /home/ubuntu/TensorRT-7.0.0.11) +``` + +## 使用 TensorRT 构建和部署 ResNet-18 + +从 MXNet ResNet-18 模型中创建 Relay 计算图: + +``` python +import tvm +from tvm import relay +import mxnet +from mxnet.gluon.model_zoo.vision import get_model + +dtype = "float32" +input_shape = (1, 3, 224, 224) +block = get_model('resnet18_v1', pretrained=True) +mod, params = relay.frontend.from_mxnet(block, shape={'data': input_shape}, dtype=dtype) +``` + +为 TensorRT 计算图进行注释和分区,TensorRT 集成的所有操作都被标记并迁移到 TensorRT,其余的操作将通过常规的 TVM CUDA 编译和代码生成。 + +``` python +from tvm.relay.op.contrib.tensorrt import partition_for_tensorrt +mod, config = partition_for_tensorrt(mod, params) +``` + +用 partition_for_tensorrt 返回的新模块和配置来构建 Relay 计算图。target 必须始终是 CUDA target。`partition_for_tensorrt` 会自动填充配置中所需的值,因此无需修改它——只需将其传给 PassContext,就可在编译时被读取到。 + +``` python +target = "cuda" +with tvm.transform.PassContext(opt_level=3, config={'relay.ext.tensorrt.options': config}): + lib = relay.build(mod, target=target, params=params) +``` + +导出模块: + +``` python +lib.export_library('compiled.so') +``` + +在目标机器上加载模块并运行推理,这个过程必须确保启用 `USE_TENSORRT_RUNTIME`。第一次运行时因为要构建 TensorRT 引擎,所以需要较长时间。 + +``` python +dev = tvm.cuda(0) +loaded_lib = tvm.runtime.load_module('compiled.so') +gen_module = tvm.contrib.graph_executor.GraphModule(loaded_lib['default'](dev)) +input_data = np.random.uniform(0, 1, input_shape).astype(dtype) +gen_module.run(data=input_data) +``` + +## 分区和编译设置 + +有些选项可在 `partition_for_tensorrt` 中配置: + +* `version` - 用(major、minor、patch)元组表示的 TensorRT 版本。若在 USE_TENSORRT_RUNTIME=ON 条件下编译 TVM,则用链接的 TensorRT 版本。这个版本会影响哪些算子可以分区到 TensorRT。 +* `use_implicit_batch` - 使用 TensorRT 隐式批处理模式(默认为 true)。设置为 false 会启用显式批处理模式,这种方式会扩大支持的算子(包括那些修改批处理维度的算子),但会降低某些模型的性能。 +* `remove_no_mac_subgraphs` - 提高性能的启发式方法。若子图没有任何乘-加操作(multiply-accumulate operation),则删除已为 TensorRT 分区的子图。删除的子图将通过 TVM 的标准编译。 +* `max_workspace_size` - 允许每个子图用于创建 TensorRT 引擎的工作空间 size(以字节为单位)。它可在运行时被覆盖。更多信息请参阅 TensorRT 文档。 + +## Runtime 设置 + +还有一些额外的选项,可在运行时用环境变量进行配置。 + +* 自动 FP16 转换 - 设置环境变量 `TVM_TENSORRT_USE_FP16=1`,从而自动将模型的 TensorRT 组件转换为 16 位浮点精度。此设置可提高性能,但可能会导致模型精度略有下降。 +* 缓存 TensorRT 引擎 - runtime 会在第一次推理时调用 TensorRT API 来构建引擎。这个过程会花费很多时间,因此可设置 `TVM_TENSORRT_CACHE_DIR` 指向磁盘上的目录,这个目录保存构建的引擎。这样下次加载模型时指定相同的目录,runtime 就会加载已经构建的引擎,从而避免较长的预热时间。每个模型的目录是唯一的。 +* TensorRT 有一个参数,用来配置模型中每一层可用的最大暂存空间量。最好使用最高值,它不会导致内存不足。可用 `TVM_TENSORRT_MAX_WORKSPACE_SIZE` 指定要用的工作区size(以字节为单位)来覆盖它。 +* 对于包含动态 batch 维度的模型,变量 `TVM_TENSORRT_MULTI_ENGINE` 可用于确定如何在 runtime 中创建 TensorRT 引擎。默认模式下 `TVM_TENSORRT_MULTI_ENGINE=0`,在内存中每次维护一个引擎。如果输入出现更高的 batch size,则用新的 max_batch_size 设置重新构建引擎——该引擎与所有 batch size(从 1 到 max_batch_size)兼容。此模式减少了运行时使用的内存量。第二种模式,`TVM_TENSORRT_MULTI_ENGINE=1` 将构建一个独特的 TensorRT 引擎,该引擎针对遇到的每个 batch size 进行了优化。这种模式下性能更佳,但内存消耗也会更多。 + +## 支持的算子 + +| Relay节点 | 备注 | +|:---|:---| +| nn.relu | | +| sigmoid | | +| tanh | | +| nn.batch_norm | | +| nn.layer_norm | | +| nn.softmax | | +| nn.conv1d | | +| nn.conv2d | | +| nn.dense | | +| nn.bias_add | | +| add | | +| subtract | | +| multiply | | +| divide | | +| power | | +| maximum | | +| minimum | | +| nn.max_pool2d | | +| nn.avg_pool2d | | +| nn.global_max_pool2d | | +| nn.global_avg_pool2d | | +| exp | | +| log | | +| sqrt | | +| abs | | +| negative | | +| nn.batch_flatten | | +| expand_dims | | +| squeeze | | +| concatenate | | +| nn.conv2d_transpose | | +| transpose | | +| layout_transform | | +| reshape | | +| nn.pad | | +| sum | | +| prod | | +| max | | +| min | | +| mean | | +| nn.adaptive_max_pool2d | | +| nn.adaptive_avg_pool2d | | +| nn.batch_matmul | | +| clip | 需要 TensorRT 的版本为 5.1.5 及以上 | +| nn.leaky_relu | 需要 TensorRT 的版本为 5.1.5 及以上 | +| sin | 需要 TensorRT 的版本为 5.1.5 及以上 | +| cos | 需要 TensorRT 的版本为 5.1.5 及以上 | +| atan | 需要 TensorRT 的版本为 5.1.5 及以上 | +| ceil | 需要 TensorRT 的版本为 5.1.5 及以上 | +| floor | 需要 TensorRT 的版本为 5.1.5 及以上 | +| split | 需要 TensorRT 的版本为 5.1.5 及以上 | +| strided_slice | 需要 TensorRT 的版本为 5.1.5 及以上 | +| nn.conv3d | 需要 TensorRT 的版本为 6.0.1 及以上 | +| nn.max_pool3d | 需要 TensorRT 的版本为 6.0.1 及以上 | +| nn.avg_pool3d | 需要 TensorRT 的版本为 6.0.1 及以上 | +| nn.conv3d_transpose | 需要 TensorRT 的版本为 6.0.1 及以上 | +| erf | 需要 TensorRT 的版本为 7.0.0 及以上 | + +## 添加新算子 + +添加对新算子的支持,需要对一系列文件进行修改: + +* *src/runtime/contrib/tensorrt/tensorrt_ops.cc* 创建一个新的算子转换器类实现 `TensorRTOpConverter` 接口。必须实现构造函数,并指定有多少输入,以及它们是张量还是权重。还必须实现 `Convert` 方法来执行转换。通过使用参数中的输入、属性和网络来添加新的 TensorRT 层,然后产生层输出。你可以使用示例中已有的转换器。最后,将新算子转换器注册到 `GetOpConverters()` 映射中。 +* *python/relay/op/contrib/tensorrt.py* 这个文件包含了 TensorRT 的注解规则(决定了支持哪些算子及其属性)。必须为 Relay 算子注册一个注解函数,并根据属性的返回值为 true 还是 false 来指定转换器支持哪些属性。 +* *tests/python/contrib/test_tensorrt.py* 为给定算子添加单元测试。 diff --git a/versioned_docs/version-0.12.0/how_to/deploy/07-vitis_ai.md b/versioned_docs/version-0.12.0/how_to/deploy/07-vitis_ai.md new file mode 100644 index 00000000..5ea3e553 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/deploy/07-vitis_ai.md @@ -0,0 +1,356 @@ +# Vitis AI 集成 + +[Vitis AI](https://github.com/Xilinx/Vitis-AI) 是用在 Xilinx 平台(包括边缘设备和 Alveo 卡)上进行硬件加速 AI 推理的 Xilinx 开发堆栈。它由优化的 IP、工具、库、模型和示例设计组成。在设计时兼顾高效率和易用性,充分发挥了 Xilinx FPGA 和 ACAP 上 AI 加速的潜力。 + +TVM 中当前的 Vitis AI 流支持使用 [Zynq Ultrascale+ MPSoc](https://www.xilinx.com/products/silicon-devices/soc/zynq-ultrascale-mpsoc.html), [Alveo](https://www.xilinx.com/products/boards-and-kits/alveo.html) 和 [Versal](https://www.xilinx.com/products/silicon-devices/acap/versal.html) 平台在边缘和云端加速神经网络模型推理。支持的边缘和云深度学习处理器单元(DPU)的标识符是: + +| **Target Board** | **DPU ID** | **TVM Target ID** | +|:---|:---|:---| +| [ZCU104](https://www.xilinx.com/products/boards-and-kits/zcu104.html) | DPUCZDX8G | DPUCZDX8G-zcu104 | +| [ZCU102](https://www.xilinx.com/products/boards-and-kits/ek-u1-zcu102-g.html) | DPUCZDX8G | DPUCZDX8G-zcu102 | +| [Kria KV260](https://www.xilinx.com/products/som/kria/kv260-vision-starter-kit.html) | DPUCZDX8G | DPUCZDX8G-kv260 | +| [VCK190](https://www.xilinx.com/products/boards-and-kits/vck190.html) | DPUCVDX8G | DPUCVDX8G | +| [VCK5000](https://www.xilinx.com/products/boards-and-kits/vck5000.html) | DPUCVDX8H | DPUCVDX8H | +| [U200](https://www.xilinx.com/products/boards-and-kits/alveo/u200.html) | DPUCADF8H | DPUCADF8H | +| [U250](https://www.xilinx.com/products/boards-and-kits/alveo/u250.html) | DPUCADF8H | DPUCADF8H | +| [U50](https://www.xilinx.com/products/boards-and-kits/alveo/u50.html) | DPUCAHX8H / DPUCAHX8L | DPUCAHX8H-u50 / DPUCAHX8L | +| [U280](https://www.xilinx.com/products/boards-and-kits/alveo/u280.html) | DPUCAHX8H / DPUCAHX8L | DPUCAHX8H-u280 / DPUCAHX8L | + +有关 DPU 标识符的更多信息,参见下表: + +| **DPU** | **Application** | **HW Platform** | **Quantization Method** | **Quantization Bitwidth** | **Design Target** | +|:---|:---|:---|:---|:---|:---| +| Deep LearningProcessing Unit | C: CNNR: RNN | AD: Alveo DDRAH: Alveo HBMVD: Versal DDR with AIE & PLZD: Zynq DDR | X: DECENTI: Integer thresholdF: Float thresholdR: RNN | 4: 4-bit8: 8-bit16: 16-bitM: Mixed Precision | G: General purposeH: High throughputL: Low latencyC: Cost optimized | + +此教程介绍有关如何在不同平台(Zynq、Alveo、Versal)上使用 Vitis AI [设置](#setup) TVM 以及如何开始 [编译模型](#compile) 并在不同平台上执行:[推理](#inference)。 + +## 系统要求 + +[Vitis AI 系统要求页面](https://github.com/Xilinx/Vitis-AI/blob/master/docs/learn/system_requirements.md) 列出了运行 Docker 容器以及在 Alveo 卡上执行的系统要求。对于边缘设备(例如 Zynq),部署模型需要使用带有 Vitis AI 流程的 TVM 编译模型的主机,以及用于运行编译模型的边缘设备。主机系统要求与上面链接中指定的相同。 + +## 设置说明 + +本节介绍如何用 Vitis AI 流为云和边缘设置 TVM。支持 Vitis AI 的 TVM 是通过 Docker 容器提供的。提供的脚本和 Dockerfile 将 TVM 和 Vitis AI 编译为单个镜像。 + +1. 克隆 TVM 仓库 + + ``` bash + git clone --recursive https://github.com/apache/tvm.git + cd tvm + ``` + +2. 构建并启动 TVM - Vitis AI Docker 容器。 + + ``` bash + ./docker/build.sh demo_vitis_ai bash + ./docker/bash.sh tvm.demo_vitis_ai + + # Setup inside container + conda activate vitis-ai-tensorflow + ``` + +3. 用 Vitis AI(在 TVM 目录内)在容器内构建 TVM + + ``` bash + mkdir build + cp cmake/config.cmake build + cd build + echo set(USE_LLVM ON) >> config.cmake + echo set(USE_VITIS_AI ON) >> config.cmake + cmake .. + make -j$(nproc) + ``` + +4. 安装 TVM + + ``` bash + cd ../python + pip3 install -e . --user + ``` + +在这个 Docker 容器中可以为云和边缘目标编译模型。要在 docker 容器内的云 Alveo 或 Versal VCK5000 卡上运行,按照 [Alveo](#alveo-setup) 或者 [Versal VCK5000](#versal-vck5000-setup) 设置说明进行操作。分别参照 [Zynq](#zynq-setup) 和 [Versal VCK190](#versal-vck190-setup),为推理过程设置 Zynq 或 Versal VCK190 评估单板。 + +### Alveo 设置 + +查看 [Alveo 设置](https://github.com/Xilinx/Vitis-AI/blob/v1.4/setup/alveo/README.md) 获取设置信息。 + +设置后,通过以下方式在 Docker 容器内选择正确的 DPU: + +``` bash +cd /workspace +git clone --branch v1.4 --single-branch --recursive https://github.com/Xilinx/Vitis-AI.git +cd Vitis-AI/setup/alveo +source setup.sh [DPU-IDENTIFIER] +``` + +可在此页面顶部的 DPU Targets 表的第二列中找到此 DPU 标识符。 + +### Versal VCK5000 设置 + +查看 [VCK5000 Setup](https://github.com/Xilinx/Vitis-AI/blob/v1.4/setup/vck5000/README.md) 获取设置信息。 + +设置后,可以通过以下方式在 Docker 容器内选择正确的 DPU: + +``` bash +cd /workspace +git clone --branch v1.4 --single-branch --recursive https://github.com/Xilinx/Vitis-AI.git +cd Vitis-AI/setup/vck5000 +source setup.sh +``` + +### Zynq 设置 + +除了构建 TVM - Vitis AI docker 之外,对于 Zynq 目标(DPUCZDX8G),编译阶段在主机上的 docker 内运行,不需要任何特定设置。执行模型时,首先要设置 Zynq 板,更多信息如下。 + +1. 下载 Petalinux 镜像: + * [ZCU104](https://www.xilinx.com/member/forms/download/design-license-xef.html?filename=xilinx-zcu104-dpu-v2021.1-v1.4.0.img.gz) + * [ZCU102](https://www.xilinx.com/member/forms/download/design-license-xef.html?filename=xilinx-zcu102-dpu-v2021.1-v1.4.0.img.gz) + * [Kria KV260](https://www.xilinx.com/member/forms/download/design-license-xef.html?filename=xilinx-kv260-dpu-v2020.2-v1.4.0.img.gz) +2. 使用 Etcher 软件将镜像文件刻录到 SD 卡上。 +3. 将带有图像的 SD 卡插入目标单板。 +4. 插入电源并使用串行端口在系统上启动该单板。 +5. 用串口设置单板的 IP 信息。有关步骤 1 至 5 的更多信息,参阅 [设置评估单板](https://www.xilinx.com/html_docs/vitis_ai/1_4/installation.html#ariaid-title8)。 +6. 在单板上创建 4GB 的交换空间 + + ``` bash + fallocate -l 4G /swapfile + chmod 600 /swapfile + mkswap /swapfile + swapon /swapfile + echo "/swapfile swap swap defaults 0 0" > /etc/fstab + ``` + +7. 安装 hdf5 依赖(需要 30 分钟到 1 小时) + + ``` bash + cd /tmp && \ + wget https://support.hdfgroup.org/ftp/HDF5/releases/hdf5-1.10/hdf5-1.10.7/src/hdf5-1.10.7.tar.gz && \ + tar -zxvf hdf5-1.10.7.tar.gz && \ + cd hdf5-1.10.7 && \ + ./configure --prefix=/usr && \ + make -j$(nproc) && \ + make install && \ + cd /tmp && rm -rf hdf5-1.10.7* + ``` + +8. 安装 Python 依赖 + + ``` bash + pip3 install Cython==0.29.23 h5py==2.10.0 pillow + ``` + +9. 安装 PyXIR + + ``` bash + git clone --recursive --branch rel-v0.3.1 --single-branch https://github.com/Xilinx/pyxir.git + cd pyxir + sudo python3 setup.py install --use_vart_edge_dpu + ``` + +10. 用 Vitis AI 构建和安装 TVM + + ``` bash + git clone --recursive https://github.com/apache/tvm + cd tvm + mkdir build + cp cmake/config.cmake build + cd build + echo set(USE_LLVM OFF) >> config.cmake + echo set(USE_VITIS_AI ON) >> config.cmake + cmake .. + make tvm_runtime -j$(nproc) + cd ../python + pip3 install --no-deps -e . + ``` + +11. 在 Python shell 中检查设置是否成功: + + ``` bash + python3 -c 'import pyxir; import tvm' + ``` + +:::note +可能会看到有关未找到 "cpu-tf" runtime 的警告,可以忽略。 +::: + +### Versal VCK190 设置 + +参考 [Zynq 设置](#zynq-setup) 设置 Versal VCK190,但在步骤 1 中参考 [VCK190 镜像](https://www.xilinx.com/member/forms/download/design-license-xef.html?filename=xilinx-vck190-dpu-v2020.2-v1.4.0.img.gz)。其他步骤相同。 + +## 编译模型 + +带有 Vitis AI 流的 TVM 包含编译和推理两个阶段。在编译期间,用户可以为当前支持的云或边缘目标设备选择要编译的模型。编译模型生成的文件可用于在 [推理](#inference) 阶段在指定的目标设备上运行模型。目前,采用 Vitis AI 流程的 TVM 支持选定数量的 Xilinx 数据中心和边缘设备。 + +本节介绍在 TVM 中用 Vitis AI 编译模型的一般流程。 + +### 导入 + +确保导入 PyXIR 和 DPU target(为 DPUCADF8H `import pyxir.contrib.target.DPUCADF8H` ): + +``` python +import pyxir +import pyxir.contrib.target.DPUCADF8H + +import tvm +import tvm.relay as relay +from tvm.contrib.target import vitis_ai +from tvm.contrib import utils, graph_executor +from tvm.relay.op.contrib.vitis_ai import partition_for_vitis_ai +``` + +### 声明 Target + +``` python +tvm_target = 'llvm' +dpu_target = 'DPUCADF8H' # options: 'DPUCADF8H', 'DPUCAHX8H-u50', 'DPUCAHX8H-u280', 'DPUCAHX8L', 'DPUCVDX8H', 'DPUCZDX8G-zcu104', 'DPUCZDX8G-zcu102', 'DPUCZDX8G-kv260' +``` + +带有 Vitis AI 流的 TVM 目前支持本页顶部表格中列出的 DPU targets。一旦定义了恰当的 target,就会调用 TVM 编译器来为指定的 target 构建计算图。 + +### 导入模型 + +导入 MXNet 模型的示例代码: + +``` python +mod, params = relay.frontend.from_mxnet(block, input_shape) +``` + +### 对模型分区 + +导入模型后,用 Relay API 为 DPU target 注释 Relay 表达式,并对计算图进行分区。 + +``` python +mod = partition_for_vitis_ai(mod, params, dpu=dpu_target) +``` + +### 构建模型 + +将分区模型传给 TVM 编译器,然后生成 TVM Runtime 的 runtime 库。 + +``` python +export_rt_mod_file = os.path.join(os.getcwd(), 'vitis_ai.rtmod') +build_options = { + 'dpu': dpu_target, + 'export_runtime_module': export_rt_mod_file +} +with tvm.transform.PassContext(opt_level=3, config={'relay.ext.vitis_ai.options': build_options}): + lib = relay.build(mod, tvm_target, params=params) +``` + +### 量化模型 + +为了用 Vitis AI DPU 加速器来加速神经网络模型的推理,通常要对模型预先量化。在 TVM - Vitis AI 流中,利用动态量化来替代此预处理步骤。在这个流中,可用典型的推理执行调用(module.run)使用提供的前 N 个输入动态量化模型(参见更多信息如下),而不需要预先量化模型。这将设置和校准 Vitis-AI DPU,为后面所有输入加速推理。 + +注意:边缘流与推理中解释的流略有不同,边缘流在前 N 个输入后模型被量化和编译,但推理不会加速,并且它可以移动到边缘设备进行部署。查看下面的 [在 Zynq 上运行](#running-on-zynq-and-vck190) 部分了解更多信息。 + +``` python +module = graph_executor.GraphModule(lib["default"](tvm.cpu())) + +# 前 N 个(默认 = 128)输入用于量化校准,并在 CPU 上执行 +# 可以通过设置 “PX_QUANT_SIZE” 来更改此配置(例如,导出 PX_QUANT_SIZE=64) +for i in range(128): + module.set_input(input_name, inputs[i]) + module.run() +``` + +用于量化的图像数量默认设置为 128,可以使用 PX_QUANT_SIZE 环境变量更改动态量化的图像数量。例如,在调用编译脚本之前在终端中执行如下命令,将量化校准数据集减少到八幅图像。 + +``` bash +export PX_QUANT_SIZE=8 +``` + +最后将 TVM 编译器的编译输出存储在磁盘上,方便在目标设备上运行模型。云 DPU(Alveo 和 VCK5000)的情况如下: + +``` python +lib_path = "deploy_lib.so" +lib.export_library(lib_path) +``` + +对于边缘 target(Zynq 和 VCK190),必须为 aarch64 重建。因此首先必须正常导出模块,并同时序列化 Vitis AI runtime 模块(vitis_ai.rtmod)。之后再次加载此 runtime 模块,为 aarch64 重建和导出。 + +``` python +temp = utils.tempdir() +lib.export_library(temp.relpath("tvm_lib.so")) + +# 为 aarch64 target 构建和导出库 +tvm_target = tvm.target.arm_cpu('ultra96') +lib_kwargs = { + 'fcompile': contrib.cc.create_shared, + 'cc': "/usr/aarch64-linux-gnu/bin/ld" +} + +build_options = { + 'load_runtime_module': export_rt_mod_file +} +with tvm.transform.PassContext(opt_level=3, config={'relay.ext.vitis_ai.options': build_options}): + lib_edge = relay.build(mod, tvm_target, params=params) + +lib_edge.export_library('deploy_lib_edge.so', **lib_kwargs) +``` + +使用 TVM 和 Vitis AI 编译模型的教程到此结束,有关如何运行已编译的模型,参阅下一节。 + +## 推理 + +带有 Vitis AI 流的 TVM 包含编译和推理两个阶段,在编译期间,用户可以选择为当前支持的任何目标设备编译模型。编译模型后生成的文件可用于在推理阶段在目标设备上运行模型。 + +查看 [在 Alveo 和 VCK5000 上运行](#running-on-alveo-and-vck5000) 以及 [在 Zynq 和 VCK190 上运行](#running-on-zynq-and-vck190) 部分,分别在云加速卡和边缘板上进行推理。 + +### 在 Alveo 和 VCK5000 上运行 + +按照编译模型部分中的步骤,可以在 Docker 内的新输入上执行如下命令以加速推理: + +``` python +module.set_input(input_name, inputs[i]) +module.run() +``` + +或者加载导出的 runtime 模块(在 [编译模型](#compile) 中导出的 deploy_lib.so): + +``` python +import pyxir +import tvm +from tvm.contrib import graph_executor + +dev = tvm.cpu() + +# input_name = ... +# input_data = ... + +# 将模块加载到内存 +lib = tvm.runtime.load_module("deploy_lib.so") + +module = graph_executor.GraphModule(lib["default"](dev)) +module.set_input(input_name, input_data) +module.run() +``` + +### 在 Zynq 和 VCK190 上运行 + +开始前按照 [Zynq](#zynq-setup) 或 [Versal VCK190](#versal-vck190-setup) 设置说明进行设置。 + +在单板上运行模型之前,需要为目标评估单板编译模型,并将编译后的模型传输到板上。如何编译模型,参阅 [编译模型](#compile) 部分。 + +之后将编译好的模型(deploy_lib_edge.so)传输到评估单板,然后可以在板上使用典型的「load_module」和「module.run」API 来执行。确保以 root 身份运行脚本(在终端中执行 `su` 登录到 root)。 + +:::note +**不要**在运行脚本(`import pyxir.contrib.target.DPUCZDX8G`)中导入 PyXIR DPU targets。 +::: + +``` python +import pyxir +import tvm +from tvm.contrib import graph_executor + +dev = tvm.cpu() + +# input_name = ... +# input_data = ... + +# 将模块加载到内存 +lib = tvm.runtime.load_module("deploy_lib_edge.so") + +module = graph_executor.GraphModule(lib["default"](dev)) +module.set_input(input_name, input_data) +module.run() +``` diff --git a/versioned_docs/version-0.12.0/how_to/deploy/08-relay_bnns.md b/versioned_docs/version-0.12.0/how_to/deploy/08-relay_bnns.md new file mode 100644 index 00000000..c69115e3 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/deploy/08-relay_bnns.md @@ -0,0 +1,123 @@ +# Relay BNNS 集成 + +**作者**:[Egor Churaev](https://github.com/echuraev) + +## 介绍 + +Apple BNNS 库由一组函数构成,这些函数用来构建推理(和训练)过程中的神经网络。macOS、iOS、tvOS 和 watchOS 支持 Apple BNNS。 BNNS 提供在这些平台上支持的所有 CPU 上执行的原语,并针对高性能和低能耗进行了优化。这种集成将尽可能多的算子从 Relay 迁移到 BNNS。 + +BNNS runtime 是平台 API 的一部分,且在现代所有 Apple 的操作系统上都可用。使用 BNNS 的应用程序不依赖额外的外部依赖。 + +BNNS 函数使用了 Apple 尚未公开的私有硬件功能,例如 AMX Apple CPU 扩展。 + +本教程演示了如何在 BNNS codegen 和 runtime 启用的情况下构建 TVM,还给出了用 BNNS runtime 编译和运行模型的示例代码,最后,还记录了支持的算子。 + +## 使用 BNNS 支持构建 TVM + +打开 USE_BNNS 标志可将 TVM BNNS codegen 和 TVM BNNS runtime 打开。 + +* USE_BNNS=ON/OFF - 将子图迁移到 BNNS 原语,并把 TVM 库链接到 BNNS runtime 模块。 + 启用此标志会搜索当前 target SDK(所需最低版本为 macOS 11.0、iOS 14.0、tvOS 14.0 和 watchOS 7.0)上的默认加速框架。 + +config.cmake 文件的设置示例: + +``` cmake +set(USE_BNNS ON) +``` + +## Relay 计算图的 BNNS 分区 + +传递模块进行编译前,必须对迁移到 BNNS 执行的操作进行注释。由 *partition_for_bnns* 注释的所有操作都将被迁移到 BNNS 上执行,其余的操作将通过 LLVM 进行编译和生成代码。 + +重要提示:BNNS 仅支持具有恒定权重的原语,因此必须将常量映射到 Relay 表示中的相关张量抽象。若要冻结张量,并将它们作为常量进行操作,则需要用特殊标志「freeze_params=True」调用 ONNX 导入器或手动绑定执行器。所有 Relay 导入器默认都不会这样做。若将 params 字典作为参数传递,可用「partition_for_bnns」来实现。 + +``` python +from tvm.relay.op.contrib.bnns import partition_for_bnns +model = partition_for_bnns(model, params=params) +``` + +## 迁移到 BNNS 执行操作的输入数据布局 + +BNNS 内核仅支持平面格式的输入数据,分区器需要 NCHW 输入布局(用于 conv2d 输入)。 + +要对混合输入布局的模型进行 BNNS 集成,应在模块传递给 *partition_for_bnns* 之前对其进行转换。布局转换只发生在显式枚举类型的操作中。根据拓扑结构,可能会围绕 conv2d 将常规数据重新排序为交织布局和平面布局。这样做会使得性能损失,并影响执行时间。建议分析整个拓扑结构,并扩展以下列表,将所有中间张量转换为 NCHW 数据布局。 + +输入布局更改的示例: + +``` python +# 对于具有 NHWC 输入布局的模型 +with tvm.transform.PassContext(opt_level=3): + mod = relay.transform.InferType()(mod) + mod = relay.transform.ConvertLayout({"nn.conv2d": ["NCHW", "default"], + "nn.bias_add": ["NCHW", "default"], + "nn.relu": ["NCHW"]})(mod) +``` + +## 示例:使用 BNNS 构建和部署 Mobilenet v2 1.0 + +从 MXNet Mobilenet v2 1.0 模型创建 Relay 计算图: + +``` python +import tvm +from tvm import relay +import mxnet +from mxnet.gluon.model_zoo.vision import get_model + +dtype = "float32" +input_shape = (1, 3, 224, 224) +block = get_model('mobilenetv2_1.0', pretrained=True) +module, params = relay.frontend.from_mxnet(block, shape={'data': input_shape}, dtype=dtype) +``` + +标记要迁移到 BNNS 原语的计算图部分,BNNS 集成支持的所有操作都将由 BNNS 调用处理,其余操作将通过常规 TVM LLVM 编译和生成代码。 + +然后将新模块编译到所需的 Apple 平台: + +``` python +from tvm.relay.op.contrib.bnns import partition_for_bnns + +# target for macOS Big Sur 11.1: +target = "llvm -mtriple=x86_64-apple-darwin20.2.0" + +model = partition_for_bnns(model, params=params) # to markup operations to be offloaded to BNNS +with tvm.transform.PassContext(opt_level=3): + lib = relay.build(model, target=target, params=params) +``` + +导出模块: + +``` python +lib.export_library('compiled.dylib') +``` + +用(在启用 `USE_BNNS` 条件下构建的) TVM 在目标机器上加载模块,并运行推理: + +``` python +import tvm +import numpy as np +from tvm.contrib import graph_executor + +dev = tvm.cpu(0) +loaded_lib = tvm.runtime.load_module('compiled.dylib') +gen_module = tvm.contrib.graph_executor.GraphModule(loaded_lib['default'](dev)) + +dtype = "float32" +input_shape = (1, 3, 224, 224) +input_data = np.random.uniform(0, 1, input_shape).astype(dtype) +gen_module.run(data=input_data) +``` + +## 支持的算子 + +| **Relay 节点** | **备注** | +|:---|:---| +| nn.conv2d | | +| nn.batch_norm | BNNS 集成仅支持 nn.conv2d-batch_norm 模式 | +| nn.dense | | +| nn.batch_matmul | | +| nn.bias_add | BNNS 集成仅支持 nn.conv2d 或 nn.dense 中融合的偏置部分 | +| add | BNNS 集成仅支持 nn.conv2d 或 nn.dense 中融合的偏置部分 | +| nn.relu | BNNS 集成仅支持 nn.conv2d 或 nn.dense 中融合的偏置部分 | +| nn.gelu | BNNS 集成仅支持 nn.conv2d 或 nn.dense 中融合的偏置部分 | + + diff --git a/versioned_docs/version-0.12.0/how_to/deploy/_category_.json b/versioned_docs/version-0.12.0/how_to/deploy/_category_.json new file mode 100644 index 00000000..16dd4f29 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/deploy/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 120 +} diff --git a/versioned_docs/version-0.12.0/how_to/deploy/deploy_models/01-deploy_android.md b/versioned_docs/version-0.12.0/how_to/deploy/deploy_models/01-deploy_android.md new file mode 100644 index 00000000..769b320d --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/deploy/deploy_models/01-deploy_android.md @@ -0,0 +1,346 @@ +--- +title: 在 Android 上部署预训练模型 +--- + +# 在 Android 上部署预训练模型 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/deploy_models/deploy_model_on_android.html#sphx-glr-download-how-to-deploy-models-deploy-model-on-android-py) 下载完整的示例代码 +::: + +**作者**:[Tomohiro Kato](https://tkat0.github.io/) + +下面是用 Relay 编译 Keras 模型,并将其部署到 Android 设备上的示例: + +``` python +import os +import numpy as np +from PIL import Image +import keras +from keras.applications.mobilenet_v2 import MobileNetV2 +import tvm +from tvm import te +import tvm.relay as relay +from tvm import rpc +from tvm.contrib import utils, ndk, graph_executor as runtime +from tvm.contrib.download import download_testdata +``` + +## 设置环境 + +由于 Android 需要的包比较多,推荐使用官方的 Docker 镜像。 + +首先,执行下面的命令来构建和运行 Docker 镜像: + +``` bash +git clone --recursive https://github.com/apache/tvm tvm +cd tvm +docker build -t tvm.demo_android -f docker/Dockerfile.demo_android ./docker +docker run --pid=host -h tvm -v $PWD:/workspace \ + -w /workspace -p 9190:9190 --name tvm -it tvm.demo_android bash +``` + +在容器中,克隆的 TVM 目录挂载到 /workspace。此时,挂载 RPC 要用的 9190 端口将在后面讨论。 + +:::note +请在容器中执行以下步骤。执行 `docker exec -it tvm bash` 在容器中打开一个新的终端。 +::: + +接下来构建 TVM: + +``` bash +mkdir build +cd build +cmake -DUSE_LLVM=llvm-config-8 \ + -DUSE_RPC=ON \ + -DUSE_SORT=ON \ + -DUSE_VULKAN=ON \ + -DUSE_GRAPH_EXECUTOR=ON \ + .. +make -j10 +``` + +TVM 构建成功后,设置 PYTHONPATH: + +``` bash +echo 'export PYTHONPATH=/workspace/python:/workspace/vta/python:${PYTHONPATH}' >> ~/.bashrc +source ~/.bashrc +``` + +## 启动 RPC 跟踪器 + +TVM 用 RPC session 与 Android 设备进行通信。 + +在容器中运行这个命令来启动 RPC 跟踪器。因为整个调优过程都需要跟踪器,因此需要为这个命令打开一个新终端: + +``` bash +python3 -m tvm.exec.rpc_tracker --host=0.0.0.0 --port=9190 +``` + +预期输出: + +``` bash +INFO:RPCTracker:bind to 0.0.0.0:9190 +``` + +## 将 Android 设备注册到 RPC 跟踪器 + +按照 [readme page](https://github.com/apache/tvm/tree/main/apps/android_rpc) 在 Android 设备上安装 TVM RPC APK。 + +下面是 config.mk 的示例(启用了 OpenCL 和 Vulkan): + +``` cmake +APP_ABI = arm64-v8a + +APP_PLATFORM = android-24 + +# 编译时是否启动 OpenCL +USE_OPENCL = 1 + +# 编译时是否启用 Vulkan +USE_VULKAN = 1 + +ifeq ($(USE_VULKAN), 1) + # 静态链接 vulkan 需要 API 级别 24 或更高 + APP_PLATFORM = android-24 +endif + +# 要添加的其他 include 头,例如 SDK_PATH/adrenosdk/Development/Inc +ADD_C_INCLUDES += /work/adrenosdk-linux-5_0/Development/Inc +# 从 https://github.com/KhronosGroup/OpenCL-Headers 下载 +ADD_C_INCLUDES += /usr/local/OpenCL-Headers/ + +# 要添加的附加链接库,例如 ANDROID_LIB_PATH/libOpenCL.so +ADD_LDLIBS = /workspace/pull-from-android-device/libOpenCL.so +``` + +:::note +不要忘记 [创建独立的工具链](https://github.com/apache/tvm/tree/main/apps/android_rpc#architecture-and-android-standalone-toolchain)。例如: + +``` bash +$ANDROID_NDK_HOME/build/tools/make-standalone-toolchain.sh \ + --platform=android-24 --use-llvm --arch=arm64 --install-dir=/opt/android-toolchain-arm64 +export TVM_NDK_CC=/opt/android-toolchain-arm64/bin/aarch64-linux-android-g++ +``` +::: + +接下来,启动 Android 应用程序,输入 RPC 跟踪器的 IP 地址和端口来注册你的设备。 + +设备注册后,可以通过查询 rpc_tracker 来确认: + +``` bash +python3 -m tvm.exec.query_rpc_tracker --host=0.0.0.0 --port=9190 +``` + +例如,如果有 1 台 Android 设备,输出为 + +``` bash +Queue Status +---------------------------------- +key total free pending +---------------------------------- +android 1 1 0 +---------------------------------- +``` + +运行下面的测试脚本,确认是否可以与 Android 通信,如果使用 OpenCL 和 Vulkan,要在脚本中设置 `test_opencl` 和 `test_vulkan`。 + +``` bash +export TVM_TRACKER_HOST=0.0.0.0 +export TVM_TRACKER_PORT=9190 +``` + +``` bash +cd /workspace/apps/android_rpc +python3 tests/android_rpc_test.py +``` + +## 加载预训练的 Keras 模型 + +加载 Keras 提供的预训练 MobileNetV2(alpha=0.5)分类模型: + +``` python +keras.backend.clear_session() # 销毁当前的 TF 计算图,并创建一个新的。 +weights_url = "".join( + [ + "https://github.com/JonathanCMitchell/", + "mobilenet_v2_keras/releases/download/v1.1/", + "mobilenet_v2_weights_tf_dim_ordering_tf_kernels_0.5_224.h5", + ] +) +weights_file = "mobilenet_v2_weights.h5" +weights_path = download_testdata(weights_url, weights_file, module="keras") +keras_mobilenet_v2 = MobileNetV2( + alpha=0.5, include_top=True, weights=None, input_shape=(224, 224, 3), classes=1000 +) +keras_mobilenet_v2.load_weights(weights_path) +``` + +为了测试模型,下载一张猫的图片,并转换其格式: + +``` python +img_url = "https://github.com/dmlc/mxnet.js/blob/main/data/cat.png?raw=true" +img_name = "cat.png" +img_path = download_testdata(img_url, img_name, module="data") +image = Image.open(img_path).resize((224, 224)) +dtype = "float32" + +def transform_image(image): + image = np.array(image) - np.array([123.0, 117.0, 104.0]) + image /= np.array([58.395, 57.12, 57.375]) + image = image.transpose((2, 0, 1)) + image = image[np.newaxis, :] + return image + +x = transform_image(image) +``` + +synset 用于将 ImageNet 类的标签,转换为人类更容易理解的单词。 + +``` python +synset_url = "".join( + [ + "https://gist.githubusercontent.com/zhreshold/", + "4d0b62f3d01426887599d4f7ede23ee5/raw/", + "596b27d23537e5a1b5751d2b0481ef172f58b539/", + "imagenet1000_clsid_to_human.txt", + ] +) +synset_name = "imagenet1000_clsid_to_human.txt" +synset_path = download_testdata(synset_url, synset_name, module="data") +with open(synset_path) as f: + synset = eval(f.read()) +``` + +## 用 Relay 编译模型 + +如果在 x86 服务器上运行示例,可将其设置为 `llvm`。如果在树莓派上运行,需要指定它的指令集。若要在真实设备上运行,需将 `local_demo` 设置为 False。 + +``` python +local_demo = True + +# 默认会在 CPU target 上执行 +# 可选值:'cpu','opencl' 和 'vulkan' +test_target = "cpu" + +# 改变 target 配置。 +# 运行 `adb shell cat /proc/cpuinfo` 命令查看 arch 的值。 +arch = "arm64" +target = tvm.target.Target("llvm -mtriple=%s-linux-android" % arch) + +if local_demo: +    target = tvm.target.Target("llvm") +elif test_target == "opencl": +    target = tvm.target.Target("opencl", host=target) +elif test_target == "vulkan": +    target = tvm.target.Target("vulkan", host=target) + +input_name = "input_1" +shape_dict = {input_name: x.shape} +mod, params = relay.frontend.from_keras(keras_mobilenet_v2, shape_dict) + +with tvm.transform.PassContext(opt_level=3): +    lib = relay.build(mod, target=target, params=params) + +# 在 `relay.build` 之后,会得到三个返回值:计算图,库和新参数,因为我们做了一些优化,它们会改变参数,但模型的结果不变。 +# 将库保存在本地临时目录中。 +tmp = utils.tempdir() +lib_fname = tmp.relpath("net.so") +fcompile = ndk.create_shared if not local_demo else None +lib.export_library(lib_fname, fcompile) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +## 通过 RPC 远程部署模型 + +利用 RPC 可将模型从主机部署到远程 Android 设备。 + +``` python +tracker_host = os.environ.get("TVM_TRACKER_HOST", "127.0.0.1") +tracker_port = int(os.environ.get("TVM_TRACKER_PORT", 9190)) +key = "android" + +if local_demo: + remote = rpc.LocalSession() +else: + tracker = rpc.connect_tracker(tracker_host, tracker_port) + # 运行重型模型时,要添加 `session_timeout` + remote = tracker.request(key, priority=0, session_timeout=60) + +if local_demo: + dev = remote.cpu(0) +elif test_target == "opencl": + dev = remote.cl(0) +elif test_target == "vulkan": + dev = remote.vulkan(0) +else: + dev = remote.cpu(0) + +# 将库上传到远程设备,并加载 +remote.upload(lib_fname) +rlib = remote.load_module("net.so") + +# 创建远程 runtime 模块 +module = runtime.GraphModule(rlib["default"](dev)) +``` + +## 在 TVM 上执行 + +``` python +# 设置输入数据 +module.set_input(input_name, tvm.nd.array(x.astype(dtype))) +# 运行 +module.run() +# 得到输出结果 +out = module.get_output(0) + +# 得到分数最高的第一个结果 +top1 = np.argmax(out.numpy()) +print("TVM prediction top-1: {}".format(synset[top1])) + +print("Evaluate inference time cost...") +print(module.benchmark(dev, number=1, repeat=10)) +``` + +输出结果: + +``` bash +TVM prediction top-1: tiger cat +Evaluate inference time cost... +Execution time summary: + mean (ms) median (ms) max (ms) min (ms) std (ms) + 15.5571 15.5695 15.7189 15.3987 0.0868 +``` + +## 样本输出 + +以下是在骁龙 820 上使用 Adreno 530 的 ‘cpu’、‘opencl’ 和 ‘vulkan’ 的结果。 + +在 GPU 上运行比 CPU 慢。为了加快速度,需要根据 GPU 架构编写和优化 schedule。 + +``` bash +# cpu +TVM prediction top-1: tiger cat +Evaluate inference time cost... +Mean inference time (std dev): 37.92 ms (19.67 ms) + +# opencl +TVM prediction top-1: tiger cat +Evaluate inference time cost... +Mean inference time (std dev): 419.83 ms (7.49 ms) + +# vulkan +TVM prediction top-1: tiger cat +Evaluate inference time cost... +Mean inference time (std dev): 465.80 ms (4.52 ms) +``` + +[下载 Python 源代码:deploy_model_on_android.py](https://tvm.apache.org/docs/_downloads/21a9dd883b196be58ca1c5cd02700274/deploy_model_on_android.py) + +[下载 Jupyter Notebook:deploy_model_on_android.ipynb](https://tvm.apache.org/docs/_downloads/eed2658f15243bab719b2de7769fa45a/deploy_model_on_android.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/deploy/deploy_models/02-deploy_nano.md b/versioned_docs/version-0.12.0/how_to/deploy/deploy_models/02-deploy_nano.md new file mode 100644 index 00000000..05fac0f8 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/deploy/deploy_models/02-deploy_nano.md @@ -0,0 +1,224 @@ +--- +title: 在 Jetson Nano 上部署预训练模型 +--- + +# 在 Jetson Nano 上部署预训练模型 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/deploy_models/deploy_model_on_nano.html#sphx-glr-download-how-to-deploy-models-deploy-model-on-nano-py) 下载完整的示例代码 +::: + +**作者**:[BBuf](https://github.com/BBuf) + +此教程介绍如何用 Relay 编译 ResNet 模型,并将其部署到 Jetson Nano。 + +``` python +import tvm +from tvm import te +import tvm.relay as relay +from tvm import rpc +from tvm.contrib import utils, graph_executor as runtime +from tvm.contrib.download import download_testdata +``` + +## 在 Jetson Nano 上构建 TVM Runtime + +第一步是在远程设备上构建 TVM runtime。 + +:::note +本节和下一节中的所有指令都应在目标设备(例如 Jetson Nano)及 Linux 上执行。 +::: + +由于我们是在本地机器上进行编译,远程设备仅用于运行生成的代码,因此只需在远程设备上构建 TVM runtime。 + +``` bash +git clone --recursive https://github.com/apache/tvm tvm +cd tvm +mkdir build +cp cmake/config.cmake build +cd build +cmake .. +make runtime -j4 +``` + +:::note +如果要用 Jetson Nano 的 GPU 进行推理,需要在 *config.cmake* 中开启 CUDA 选项,即 *set(USE_CUDA ON)*。 +::: + +runtime 构建完成后,在 `~/.bashrc` 文件中设置环境变量。可以用 `vi ~/.bashrc` 命令来编辑 `~/.bashrc`,添加下面这行代码(假设 TVM 目录在 `~/tvm` 中): + +``` bash +export PYTHONPATH=$PYTHONPATH:~/tvm/python +``` + +执行 `source ~/.bashrc` 来更新环境变量。 + +## 在设备上设置 RPC 服务器 + +若要启动 RPC 服务器,请在远程设备(示例中为 Jetson Nano)上运行以下命令: + +``` bash +python -m tvm.exec.rpc_server --host 0.0.0.0 --port=9091 +``` + +看到如下结果,则表示 RPC 服务器启动成功: + +``` bash +INFO:RPCServer:bind to 0.0.0.0:9091 +``` + +## 准备预训练模型 + +注意:确保主机已经(用 LLVM)安装了完整的 TVM。 + +使用 [MXNet Gluon 模型集合](https://mxnet.apache.org/api/python/gluon/model_zoo.html) 中的预训练模型。更多有关这部分的信息详见 [编译 MXNet 模型](../../compile/compile_mxnet) 教程。 + +``` python +from mxnet.gluon.model_zoo.vision import get_model +from PIL import Image +import numpy as np + +# 获取模型 +block = get_model("resnet18_v1", pretrained=True) +``` + +为了测试模型,下载一张猫的图片,并转换其格式: + +``` python +img_url = "https://github.com/dmlc/mxnet.js/blob/main/data/cat.png?raw=true" +img_name = "cat.png" +img_path = download_testdata(img_url, img_name, module="data") +image = Image.open(img_path).resize((224, 224)) + +def transform_image(image): + image = np.array(image) - np.array([123.0, 117.0, 104.0]) + image /= np.array([58.395, 57.12, 57.375]) + image = image.transpose((2, 0, 1)) + image = image[np.newaxis, :] + return image + +x = transform_image(image) +``` + +用 synset 将 ImageNet 类的标签转换为人类更容易理解的单词。 + +``` python +synset_url = "".join( + [ + "https://gist.githubusercontent.com/zhreshold/", + "4d0b62f3d01426887599d4f7ede23ee5/raw/", + "596b27d23537e5a1b5751d2b0481ef172f58b539/", + "imagenet1000_clsid_to_human.txt", + ] +) +synset_name = "imagenet1000_clsid_to_human.txt" +synset_path = download_testdata(synset_url, synset_name, module="data") +with open(synset_path) as f: + synset = eval(f.read()) +``` + +以下代码可将 Gluon 模型移植到可移植的计算图上: + +``` python +# 在 mxnet.gluon 中支持 MXNet 静态图(符号)和 HybridBlock +shape_dict = {"data": x.shape} +mod, params = relay.frontend.from_mxnet(block, shape_dict) +# 添加 softmax 算子提高概率 +func = mod["main"] +func = relay.Function(func.params, relay.nn.softmax(func.body), None, func.type_params, func.attrs) +``` + +以下是一些基本的数据工作负载配置: + +``` python +batch_size = 1 +num_classes = 1000 +image_shape = (3, 224, 224) +data_shape = (batch_size,) + image_shape +``` + +## 编译计算图 + +用计算图的配置和参数调用 `relay.build()` 函数,从而编译计算图。但是,不能在具有 ARM 指令集的设备上部署 x86 程序。除了用来指定深度学习工作负载的参数 `net` 和 `params`,Relay 还需要知道目标设备的编译选项。不同选项会导致性能不同。 + +如果在 x86 服务器上运行示例,可将其设置为 `llvm`。如果在 Jetson Nano 上运行,需要将其设置为 `nvidia/jetson-nano`。若要在真实设备上运行,需将 `local_demo` 设置为 False。 + +``` python +local_demo = True + +if local_demo: + target = tvm.target.Target("llvm") +else: + target = tvm.target.Target("nvidia/jetson-nano") + assert target.kind.name == "cuda" + assert target.attrs["arch"] == "sm_53" + assert target.attrs["shared_memory_per_block"] == 49152 + assert target.attrs["max_threads_per_block"] == 1024 + assert target.attrs["thread_warp_size"] == 32 + assert target.attrs["registers_per_block"] == 32768 + +with tvm.transform.PassContext(opt_level=3): + lib = relay.build(func, target, params=params) + +# 在 `relay.build` 之后,会得到三个返回值:计算图,库和新参数,因为我们做了一些优化,它们会改变参数,但模型的结果不变。 + +# 将库保存在本地临时目录中。 +tmp = utils.tempdir() +lib_fname = tmp.relpath("net.tar") +lib.export_library(lib_fname) +``` + +输出结果: + +``` bash +/workspace/python/tvm/relay/build_module.py:348: DeprecationWarning: Please use input parameter mod (tvm.IRModule) instead of deprecated parameter mod (tvm.relay.function.Function) + DeprecationWarning, +/workspace/python/tvm/driver/build_module.py:267: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +## 通过 RPC 远程部署模型 + +利用 RPC 可将模型从主机部署到远程设备。 + +``` python +# 从远程设备获取 RPC session。 +if local_demo: + remote = rpc.LocalSession() +else: + # 下面是教程环境,把这个改成你目标设备的 IP 地址 + host = "192.168.1.11" + port = 9091 + remote = rpc.connect(host, port) + +# 将库上传到远程设备,并加载它 +remote.upload(lib_fname) +rlib = remote.load_module("net.tar") + +# 创建远程 runtime 模块 +if local_demo: + dev = remote.cpu(0) +else: + dev = remote.cuda(0) + +module = runtime.GraphModule(rlib["default"](dev)) +# 设置输入数据 +module.set_input("data", tvm.nd.array(x.astype("float32"))) +# 运行 +module.run() +# 获取输出 +out = module.get_output(0) +# 得到排第一的结果 +top1 = np.argmax(out.numpy()) +print("TVM prediction top-1: {}".format(synset[top1])) +``` + +输出结果: + +``` bash +TVM prediction top-1: tiger cat +``` + +[下载 Python 源代码:deploy_model_on_nano.py](https://tvm.apache.org/docs/_downloads/3fde0fe8b31bf786dec2a01858372eae/deploy_model_on_nano.py) + +[下载 Jupyter Notebook:deploy_model_on_nano.ipynb](https://tvm.apache.org/docs/_downloads/cafefaac0e14b00fd7644da616cab35a/deploy_model_on_nano.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/deploy/deploy_models/03-deploy_pi.md b/versioned_docs/version-0.12.0/how_to/deploy/deploy_models/03-deploy_pi.md new file mode 100644 index 00000000..394a7e94 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/deploy/deploy_models/03-deploy_pi.md @@ -0,0 +1,212 @@ +--- +title: 在树莓派上部署预训练模型 +--- + +# 在树莓派上部署预训练模型 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/deploy_models/deploy_model_on_rasp.html#sphx-glr-download-how-to-deploy-models-deploy-model-on-rasp-py) 下载完整的示例代码 +::: + +**作者**:[Ziheng Jiang](https://ziheng.org/),[Hiroyuki Makino](https://makihiro.github.io/) + +此教程介绍如何用 Relay 编译 ResNet 模型,并将其部署到树莓派。 + +``` python +import tvm +from tvm import te +import tvm.relay as relay +from tvm import rpc +from tvm.contrib import utils, graph_executor as runtime +from tvm.contrib.download import download_testdata +``` + +## 在设备上构建 TVM Runtime + +首先在远程设备上构建 TVM runtime。 + +:::note +本节和下一节中的所有指令都应在目标设备(例如树莓派)及 Linux 上执行。 +::: + +由于我们是在本地机器上进行编译,远程设备仅用于运行生成的代码,因此只需在远程设备上构建 TVM runtime。 + +``` bash +git clone --recursive https://github.com/apache/tvm tvm +cd tvm +mkdir build +cp cmake/config.cmake build +cd build +cmake .. +make runtime -j4 +``` + +runtime 构建完成后,在 `~/.bashrc` 文件中设置环境变量——用 `vi ~/.bashrc` 命令来编辑 `~/.bashrc`,添加下面这行代码(假设 TVM 目录在 `~/tvm` 中): + +``` bash +export PYTHONPATH=$PYTHONPATH:~/tvm/python +``` + +执行 `source ~/.bashrc` 来更新环境变量。 + +## 在设备上设置 RPC 服务器 + +在远程设备(该示例中为树莓派)上运行以下命令,启动 RPC 服务器: + +``` bash +python -m tvm.exec.rpc_server --host 0.0.0.0 --port=9090 +``` + +看到如下结果,则表示 RPC 服务器启动成功: + +``` bash +INFO:root:RPCServer: bind to 0.0.0.0:9090 +``` + +## 准备预训练模型 + +注意:确保主机已经(用 LLVM)安装了完整的 TVM。 + +使用 [MXNet Gluon 模型集合](https://mxnet.apache.org/api/python/gluon/model_zoo.html) 中的预训练模型。更多有关这部分的信息详见 [编译 MXNet 模型](/docs/how_to/compile/compile_mxnet) 教程。 + +``` python +from mxnet.gluon.model_zoo.vision import get_model +from PIL import Image +import numpy as np + +# 用一行代码来获取模型 +block = get_model("resnet18_v1", pretrained=True) +``` + +为了测试模型,下载一张猫的图片,并转换其格式: + +``` python +img_url = "https://github.com/dmlc/mxnet.js/blob/main/data/cat.png?raw=true" +img_name = "cat.png" +img_path = download_testdata(img_url, img_name, module="data") +image = Image.open(img_path).resize((224, 224)) + +def transform_image(image): + image = np.array(image) - np.array([123.0, 117.0, 104.0]) + image /= np.array([58.395, 57.12, 57.375]) + image = image.transpose((2, 0, 1)) + image = image[np.newaxis, :] + return image + +x = transform_image(image) +``` + +synset 用于将 ImageNet 类的标签,转换为人类更容易理解的单词。 + +``` python +synset_url = "".join( + [ + "https://gist.githubusercontent.com/zhreshold/", + "4d0b62f3d01426887599d4f7ede23ee5/raw/", + "596b27d23537e5a1b5751d2b0481ef172f58b539/", + "imagenet1000_clsid_to_human.txt", + ] +) +synset_name = "imagenet1000_clsid_to_human.txt" +synset_path = download_testdata(synset_url, synset_name, module="data") +with open(synset_path) as f: + synset = eval(f.read()) +``` + +以下代码可将 Gluon 模型移植到可移植的计算图上: + +``` python +# 在 mxnet.gluon 中支持 MXNet 静态计算图(符号)和 HybridBlock +shape_dict = {"data": x.shape} +mod, params = relay.frontend.from_mxnet(block, shape_dict) +# 添加 softmax 算子提高概率 +func = mod["main"] +func = relay.Function(func.params, relay.nn.softmax(func.body), None, func.type_params, func.attrs) +``` + +以下是一些基本的数据工作负载配置: + +``` python +batch_size = 1 +num_classes = 1000 +image_shape = (3, 224, 224) +data_shape = (batch_size,) + image_shape +``` + +## 编译计算图 + +用计算图的配置和参数调用 `relay.build()` 函数,从而编译计算图。但是,不能在具有 ARM 指令集的设备上部署 x86 程序。除了用来指定深度学习工作负载的参数 `net` 和 `params`,Relay 还需要知道目标设备的编译选项。不同选项会导致性能不同。 + +如果在 x86 服务器上运行示例,可将其设置为 `llvm`。如果在树莓派上运行,需要指定它的指令集。若要在真实设备上运行,需将 `local_demo` 设置为 False。 + +``` python +local_demo = True + +if local_demo: + target = tvm.target.Target("llvm") +else: + target = tvm.target.arm_cpu("rasp3b") + # 上面一行代码是下面代码的简单形式: + # target = tvm.target.Target('llvm -device=arm_cpu -model=bcm2837 -mtriple=armv7l-linux-gnueabihf -mattr=+neon') + +with tvm.transform.PassContext(opt_level=3): + lib = relay.build(func, target, params=params) + +# 在 `relay.build` 之后,会得到三个返回值:计算图,库和新参数,因为我们做了一些优化,它们会改变参数,但模型的结果不变。 + +# 将库保存在本地临时目录中。 +tmp = utils.tempdir() +lib_fname = tmp.relpath("net.tar") +lib.export_library(lib_fname) +``` + +输出结果: + +``` bash +/workspace/python/tvm/relay/build_module.py:411: DeprecationWarning: Please use input parameter mod (tvm.IRModule) instead of deprecated parameter mod (tvm.relay.function.Function) + DeprecationWarning, +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +## 通过 RPC 远程部署模型 + +利用 RPC 可将模型从主机部署到远程设备。 + +``` python +# 从远程设备获取 RPC session。 +if local_demo: + remote = rpc.LocalSession() +else: + # 下面是教程环境,把这个改成你目标设备的 IP 地址 + host = "10.77.1.162" + port = 9090 + remote = rpc.connect(host, port) + +# 将库上传到远程设备,并加载它 +remote.upload(lib_fname) +rlib = remote.load_module("net.tar") + +# 创建远程 runtime 模块 +dev = remote.cpu(0) +module = runtime.GraphModule(rlib["default"](dev)) +# 设置输入数据 +module.set_input("data", tvm.nd.array(x.astype("float32"))) +# 运行 +module.run() +# 得到结果 +out = module.get_output(0) +# 得到排第一的结果 +top1 = np.argmax(out.numpy()) +print("TVM prediction top-1: {}".format(synset[top1])) +``` + +输出结果: + +``` bash +TVM prediction top-1: tiger cat +``` + +[下载 Python 源代码:deploy_model_on_rasp.py](https://tvm.apache.org/docs/_downloads/408ef96692a668b33d94eca33cac7e0a/deploy_model_on_rasp.py) + +[下载 Jupyter Notebook:deploy_model_on_rasp.ipynb](https://tvm.apache.org/docs/_downloads/7c392f39b90d93406ef30c6185c5686c/deploy_model_on_rasp.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/deploy/deploy_models/04-compile_od.md b/versioned_docs/version-0.12.0/how_to/deploy/deploy_models/04-compile_od.md new file mode 100644 index 00000000..040a89af --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/deploy/deploy_models/04-compile_od.md @@ -0,0 +1,199 @@ +--- +title: 编译 PyTorch 目标检测模型 +--- + +# 编译 PyTorch 目标检测模型 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/deploy_models/deploy_object_detection_pytorch.html#sphx-glr-download-how-to-deploy-models-deploy-object-detection-pytorch-py) 下载完整的示例代码 +::: + +本文介绍如何用 Relay VM 部署 PyTorch 目标检测模型。 + +首先应安装 PyTorch。此外,还应安装 TorchVision,并将其作为模型合集(model zoo)。 + +可通过 pip 快速安装: + +``` bash +pip install torch==1.7.0 +pip install torchvision==0.8.1 +``` + +或参考官网:https://pytorch.org/get-started/locally/ + +PyTorch 版本应该和 TorchVision 版本兼容。 + +目前 TVM 支持 PyTorch 1.7 和 1.4,其他版本可能不稳定。 + +``` python +import tvm +from tvm import relay +from tvm import relay +from tvm.runtime.vm import VirtualMachine +from tvm.contrib.download import download_testdata + +import numpy as np +import cv2 + +# PyTorch 导入 +import torch +import torchvision +``` + +## 从 TorchVision 加载预训练的 MaskRCNN 并进行跟踪 + +``` python +in_size = 300 +input_shape = (1, 3, in_size, in_size) + +def do_trace(model, inp): + model_trace = torch.jit.trace(model, inp) + model_trace.eval() + return model_trace + +def dict_to_tuple(out_dict): + if "masks" in out_dict.keys(): + return out_dict["boxes"], out_dict["scores"], out_dict["labels"], out_dict["masks"] + return out_dict["boxes"], out_dict["scores"], out_dict["labels"] + +class TraceWrapper(torch.nn.Module): + def __init__(self, model): + super().__init__() + self.model = model + + def forward(self, inp): + out = self.model(inp) + return dict_to_tuple(out[0]) + +model_func = torchvision.models.detection.maskrcnn_resnet50_fpn +model = TraceWrapper(model_func(pretrained=True)) + +model.eval() +inp = torch.Tensor(np.random.uniform(0.0, 250.0, size=(1, 3, in_size, in_size))) + +with torch.no_grad(): + out = model(inp) + script_module = do_trace(model, inp) +``` + +输出结果: + +``` bash +Downloading: "https://download.pytorch.org/models/maskrcnn_resnet50_fpn_coco-bf2d0c1e.pth" to /workspace/.cache/torch/hub/checkpoints/maskrcnn_resnet50_fpn_coco-bf2d0c1e.pth + + 0%| | 0.00/170M [00:00 score_threshold: + valid_boxes.append(boxes[i]) + else: + break + +print("Get {} valid boxes".format(len(valid_boxes))) +``` + +输出结果: + +``` bash +Get 9 valid boxes +``` + +**脚本总运行时长:**(2 分 57.278 秒) + +[下载 Python 源代码:deploy_object_detection_pytorch.py](https://tvm.apache.org/docs/_downloads/7795da4b258c8feff986668b95ef57ad/deploy_object_detection_pytorch.py) + +[下载 Jupyter Notebook:deploy_object_detection_pytorch.ipynb](https://tvm.apache.org/docs/_downloads/399e1d7889ca66b69d51655784827503/deploy_object_detection_pytorch.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/deploy/deploy_models/05-deploy_prequan.md b/versioned_docs/version-0.12.0/how_to/deploy/deploy_models/05-deploy_prequan.md new file mode 100644 index 00000000..a75483ad --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/deploy/deploy_models/05-deploy_prequan.md @@ -0,0 +1,266 @@ +--- +title: 使用 TVM 部署框架预量化模型 +--- + +# 使用 TVM 部署框架预量化模型 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/deploy_models/deploy_prequantized.html#sphx-glr-download-how-to-deploy-models-deploy-prequantized-py) 下载完整的示例代码 +::: + +**作者**:[Masahiro Masuda](https://github.com/masahi) + +本文介绍如何将深度学习框架量化的模型加载到 TVM。预量化模型的导入是 TVM 中支持的量化之一。有关 TVM 中量化的更多信息,参阅 [此处](https://discuss.tvm.apache.org/t/quantization-story/3920)。 + +这里演示了如何加载和运行由 PyTorch、MXNet 和 TFLite 量化的模型。加载后,可以在任何 TVM 支持的硬件上运行编译后的量化模型。 + +首先,导入必要的包: + +``` python +from PIL import Image +import numpy as np +import torch +from torchvision.models.quantization import mobilenet as qmobilenet + +import tvm +from tvm import relay +from tvm.contrib.download import download_testdata +``` + +定义运行 demo 的辅助函数: + +``` python +def get_transform(): + import torchvision.transforms as transforms + + normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) + return transforms.Compose( + [ + transforms.Resize(256), + transforms.CenterCrop(224), + transforms.ToTensor(), + normalize, + ] + ) + +def get_real_image(im_height, im_width): + img_url = "https://github.com/dmlc/mxnet.js/blob/main/data/cat.png?raw=true" + img_path = download_testdata(img_url, "cat.png", module="data") + return Image.open(img_path).resize((im_height, im_width)) + +def get_imagenet_input(): + im = get_real_image(224, 224) + preprocess = get_transform() + pt_tensor = preprocess(im) + return np.expand_dims(pt_tensor.numpy(), 0) + +def get_synset(): + synset_url = "".join( + [ + "https://gist.githubusercontent.com/zhreshold/", + "4d0b62f3d01426887599d4f7ede23ee5/raw/", + "596b27d23537e5a1b5751d2b0481ef172f58b539/", + "imagenet1000_clsid_to_human.txt", + ] + ) + synset_name = "imagenet1000_clsid_to_human.txt" + synset_path = download_testdata(synset_url, synset_name, module="data") + with open(synset_path) as f: + return eval(f.read()) + +def run_tvm_model(mod, params, input_name, inp, target="llvm"): + with tvm.transform.PassContext(opt_level=3): + lib = relay.build(mod, target=target, params=params) + + runtime = tvm.contrib.graph_executor.GraphModule(lib["default"](tvm.device(target, 0))) + + runtime.set_input(input_name, inp) + runtime.run() + return runtime.get_output(0).numpy(), runtime +``` + +从标签到类名的映射,验证模型的输出是否合理: + +``` python +synset = get_synset() +``` + +用猫的图像进行演示: + +``` python +inp = get_imagenet_input() +``` + +## 部署量化的 PyTorch 模型 + +首先演示如何用 PyTorch 前端加载由 PyTorch 量化的深度学习模型。 + +参考 [PyTorch 静态量化教程](https://pytorch.org/tutorials/advanced/static_quantization_tutorial.html),了解量化的工作流程。 + +用下面的函数来量化 PyTorch 模型。此函数采用浮点模型,并将其转换为 uint8。这个模型是按通道量化的。 + +``` python +def quantize_model(model, inp): + model.fuse_model() + model.qconfig = torch.quantization.get_default_qconfig("fbgemm") + torch.quantization.prepare(model, inplace=True) + # Dummy calibration + model(inp) + torch.quantization.convert(model, inplace=True) +``` + +## 从 torchvision 加载预量化、预训练的 Mobilenet v2 模型 + +之所以选择 mobilenet v2,是因为该模型接受了量化感知训练,而其他模型则需要完整的训练后校准。 + +``` python +qmodel = qmobilenet.mobilenet_v2(pretrained=True).eval() +``` + +输出结果: + +``` bash +Downloading: "https://download.pytorch.org/models/mobilenet_v2-b0353104.pth" to /workspace/.cache/torch/hub/checkpoints/mobilenet_v2-b0353104.pth + + 0%| | 0.00/13.6M [00:00= calibration_samples: + break + data, _ = batch_fn(batch) + yield {"data": data} +``` + +## 导入模型 + +用 Relay MxNet 前端从 Gluon 模型集合(model zoo)中导入模型。 + +``` python +def get_model(): + gluon_model = gluon.model_zoo.vision.get_model(model_name, pretrained=True) + img_size = 299 if model_name == "inceptionv3" else 224 + data_shape = (batch_size, 3, img_size, img_size) + mod, params = relay.frontend.from_mxnet(gluon_model, {"data": data_shape}) + return mod, params +``` + +## 量化模型 + +量化过程要找到每一层的每个权重和中间特征图(feature map)张量的 scale。 + +对于权重而言,scales 是根据权重的值直接计算出来的。支持两种模式:power2 和 max。这两种模式都是先找到权重张量内的最大值。在 power2 模式下,最大值向下舍入为 2 的幂。如果权重和中间特征图的 scale 都是 2 的幂,则可以利用移位(bit shifting)进行乘法运算,这使得计算效率更高。在 max 模式下,最大值用作 scale。如果不进行四舍五入,在某些情况下 max 模式可能具有更好的精度。当 scale 不是 2 的幂时,将使用定点乘法。 + +中间特征图可以通过数据感知量化来找到 scale。数据感知量化将校准数据集作为输入参数,通过最小化量化前后激活分布之间的 KL 散度来计算 scales。或者也可以用预定义的全局 scales,这样可以节省校准时间,但会影响准确性。 + +``` python +def quantize(mod, params, data_aware): + if data_aware: + with relay.quantize.qconfig(calibrate_mode="kl_divergence", weight_scale="max"): + mod = relay.quantize.quantize(mod, params, dataset=calibrate_dataset()) + else: + with relay.quantize.qconfig(calibrate_mode="global_scale", global_scale=8.0): + mod = relay.quantize.quantize(mod, params) + return mod +``` + +## 运行推理 + +创建一个 Relay VM 来构建和执行模型。 + +``` python +def run_inference(mod): + model = relay.create_executor("vm", mod, dev, target).evaluate() + val_data, batch_fn = get_val_data() + for i, batch in enumerate(val_data): + data, label = batch_fn(batch) + prediction = model(data) + if i > 10: # 本教程只对几个样本进行推理 + break + +def main(): + mod, params = get_model() + mod = quantize(mod, params, data_aware=True) + run_inference(mod) + +if __name__ == "__main__": + main() +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +/workspace/python/tvm/relay/build_module.py:411: DeprecationWarning: Please use input parameter mod (tvm.IRModule) instead of deprecated parameter mod (tvm.relay.function.Function) + DeprecationWarning, +``` + +**脚本总运行时长:**(1 分 22.338 秒) + +[下载 Python 源代码:deploy_quantized.py](https://tvm.apache.org/docs/_downloads/7810ecf51bfc05f7d5e8a400ac3e815d/deploy_quantized.py) + +[下载 Jupyter Notebook:deploy_quantized.ipynb](https://tvm.apache.org/docs/_downloads/a269cb38341b190be980a0bd3ea8a625/deploy_quantized.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/deploy/deploy_models/08-hugging_face.md b/versioned_docs/version-0.12.0/how_to/deploy/deploy_models/08-hugging_face.md new file mode 100644 index 00000000..5e08711c --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/deploy/deploy_models/08-hugging_face.md @@ -0,0 +1,299 @@ +--- +title: 在 CPU 上部署 Hugging Face 剪枝模型 +--- + +# 在 CPU 上部署 Hugging Face 剪枝模型 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/deploy_models/deploy_sparse.html#sphx-glr-download-how-to-deploy-models-deploy-sparse-py) 下载完整的示例代码 +::: + +**作者**:[Josh Fromm](https://github.com/jwfromm) + +本教程演示如何采用剪枝后的模型(本例中模型是 [来自 Hugging Face 的 PruneBert](https://huggingface.co/huggingface/prunebert-base-uncased-6-finepruned-w-distil-squad)),并使用 TVM 来利用模型稀疏支持来加速。 + +尽管本教程的主要目的是在已经修剪过的模型上实现加速,但评估修剪后模型的速度也十分必要。为此,我们提供了一个函数采用未修剪的模型,并将其权重替换为指定稀疏的随机和修剪权重。确定模型是否值得修剪时,这可能是一个有用的特性。 + +进入代码前讨论一下稀疏和剪枝,并深入研究两种不同类型的稀疏:**结构化**和**非结构化**。 + +剪枝是一种主要通过将权重值替换为 0 来减小模型参数大小的技术,尽管选择哪些权重设置为 0 的方法众多,但最直接的方法是选择具有最小值的权重。 + +通常,权重会被修剪为所需的稀疏百分比。例如,一个 95% 的稀疏模型将只有 5% 的权重非零。修剪成非常高的稀疏通常需要微调,或完全重新训练,因为它是有损近似。尽管通过简单的压缩从修剪后的模型中很容易获得参数大小的好处,但利用模型稀疏来产生 runtime 加速更加复杂。 + +修剪结构化稀疏权重的目的,是把修剪过的权重聚集在一起。换言之,用它们的值和位置进行修剪。将修剪后的权重捆绑在一起的好处是它允许诸如矩阵乘法之类的算法跳过整个块。 + +事实证明,在当今可用的大多数硬件上,某种程度的*块稀疏*对于实现显著加速非常重要。这是因为在大多数 CPU 或 GPU 加载内存时,一次跳过读取单个值并不会节省任何工作,而是使用向量化指令之类的东西读入并执行整个块。 + +非结构化稀疏权重是仅根据原始权重值进行修剪的权重,它们看起来随机分散在整个张量中,而非像块稀疏权重中看到的那样成块。在低稀疏下,非结构化剪枝技术很难加速。然而,在高稀疏下,会出现许多全 0 值的块,这使得加速成为可能。 + +本教程包含结构化和非结构化稀疏。Hugging Face 的 PruneBert 模型是非结构化的,但 95% 是稀疏的,即使不是最优的,也可以对其应用 TVM 的块稀疏优化。 + +可以用结构化稀疏为未修剪模型生成随机稀疏权重。将 PruneBert 的真实速度与使用假权重的块稀疏速度比较,可以发现结构化稀疏的优势。 + +## 加载所需模块 + +除了 TVM,还需要 scipy(最新的 transformers)和 TensorFlow(版本在 2.2 以上)。 + +``` python +import os +import tvm +import time +import itertools +import numpy as np +import tensorflow as tf +from tvm import relay, runtime +from tvm.contrib import graph_executor +from tvm.relay import data_dep_optimization as ddo +from tensorflow.python.framework.convert_to_constants import ( + convert_variables_to_constants_v2, +) +import scipy.sparse as sp + +# 要求 TensorFlow 将其 GPU 内存限制为实际需要的内存 +# 而不是任其消耗其他内存。 +# https://www.tensorflow.org/guide/gpu#limiting_gpu_memory_growth +# 这样对 sphinx-gallery 更友好一点。 +gpus = tf.config.list_physical_devices("GPU") +if gpus: + try: + for gpu in gpus: + tf.config.experimental.set_memory_growth(gpu, True) + print("tensorflow will use experimental.set_memory_growth(True)") + except RuntimeError as e: + print("experimental.set_memory_growth option is not available: {}".format(e)) +``` + +## 配置设置 + +从参数开始,定义要运行的模型类型和稀疏。 + +``` python +# 要下载和运行的 transformer 模型的名称 +name = "huggingface/prunebert-base-uncased-6-finepruned-w-distil-squad" +# 输入的 batches 数目 +batch_size = 1 +# 每个输入序列的长度。 +seq_len = 128 +# TVM 平台标识符。注意,可以通过设置 -mcpu 来实现最佳 CPU 性能 +# 适合特定机器,还支持 CUDA 和 ROCm。 +target = "llvm" +# 在哪个设备上运行,tvm.cpu() 或 tvm.cuda() 。 +# 如果为 True,则将运行网络的稀疏变体,并进行 benchmark 测试。 +measure_sparse = True +# 结构化稀疏块大小转换权重张量 +# 进入。更改此参数可能会提高某些平台的速度。 +bs_r = 1 +# 对于除 PruneBert(95% 稀疏)以外的模型,此参数 +# 确定生成的权重稀疏,值越大,稀疏越高,结果越快。 +sparsity = 0.85 +``` + +## 下载和转换 Transformers 模型 + +下面从 transformers 模块获取一个模型,下载并转换为 TensorFlow graphdef,将该 graphdef 转换为可以优化和部署的 Relay 计算图。 + +``` python +def load_keras_model(module, name, seq_len, batch_size, report_runtime=True): + model = module.from_pretrained(name) + dummy_input = tf.keras.Input(shape=[seq_len], batch_size=batch_size, dtype="int32") + dummy_out = model(dummy_input) # 通过 Keras 模型传播 shape。 + if report_runtime: + np_input = np.random.uniform(size=[batch_size, seq_len], low=0, high=seq_len).astype( + "int32" + ) + start = time.time() + repeats = 50 + for i in range(repeats): + np_out = model(np_input) + end = time.time() + print("Keras Runtime: %f ms." % (1000 * ((end - start) / repeats))) + return model + +def convert_to_graphdef(model, batch_size, seq_len): + model_func = tf.function(lambda x: model(x)) + input_dict = model._saved_model_inputs_spec + input_spec = input_dict[list(input_dict.keys())[0]] + model_func = model_func.get_concrete_function( + tf.TensorSpec([batch_size, seq_len], input_spec.dtype) + ) + frozen_func = convert_variables_to_constants_v2(model_func) + return frozen_func.graph.as_graph_def() + +def download_model(name, batch_size, seq_len): + import transformers + + module = getattr(transformers, "TFBertForSequenceClassification") + model = load_keras_model(module, name=name, batch_size=batch_size, seq_len=seq_len) + return convert_to_graphdef(model, batch_size, seq_len) +``` + +## 转换为 Relay 计算图 + +目前有很多工具可以获得正确格式的 transformers 模型,从而进行 Relay 转换。下面的函数将导入的计算图保存为 Relay 的 json 格式,这样就不必在每次运行此脚本时从 TensorFlow 重新导入了。 + +``` python +def import_graphdef( + name, + batch_size, + seq_len, + save_relay=True, + relay_file="model.json", + relay_params="model.params", +): + abs_path = os.path.dirname(os.path.abspath(__file__)) + shape_dict = {"input_1": (batch_size, seq_len)} + relay_file = ("%s_%d_%d_%s" % (name, batch_size, seq_len, relay_file)).replace("/", "_") + relay_params = ("%s_%d_%d_%s" % (name, batch_size, seq_len, relay_params)).replace("/", "_") + if os.path.exists(os.path.join(abs_path, relay_file)) and os.path.exists( + os.path.join(abs_path, relay_params) + ): + with open(os.path.join(abs_path, relay_file), "r") as fi: + mod = tvm.ir.load_json(fi.read()) + with open(os.path.join(abs_path, relay_params), "rb") as fi: + params = relay.load_param_dict(fi.read()) + else: + graph_def = download_model(name, batch_size, seq_len) + + mod, params = relay.frontend.from_tensorflow(graph_def, shape=shape_dict) + + if save_relay: + with open(os.path.join(abs_path, relay_file), "w") as fo: + fo.write(tvm.ir.save_json(mod)) + with open(os.path.join(abs_path, relay_params), "wb") as fo: + fo.write(runtime.save_param_dict(params)) + + return mod, dict(params.items()), shape_dict +``` + +## 运行密集计算图 + +运行导入模型的默认版本。注意,即使权重是稀疏的,也不会看到任何加速,因为在这些密集(但大部分为零)张量上使用的是常规密集矩阵乘法,而非稀疏感知内核。 + +``` python +def run_relay_graph(mod, params, shape_dict, target, dev): + with relay.build_config(opt_level=3): + lib = relay.build(mod, target=target, params=params) + input_shape = shape_dict["input_1"] + dummy_data = np.random.uniform(size=input_shape, low=0, high=input_shape[1]).astype("int32") + + m = graph_executor.GraphModule(lib["default"](dev)) + m.set_input(0, dummy_data) + m.run() + tvm_output = m.get_output(0) + + print(m.benchmark(dev, repeat=5, number=5)) + return tvm_output + +def run_dense(mod, params, shape_dict, target, dev): + print("Dense Model Benchmark:") + return run_relay_graph(mod, params, shape_dict, target, dev) +``` + +## 运行稀疏计算图 + +接下来把计算图转换为稀疏表示,并在需要时生成假的稀疏权重。然后用与 dense 相同的 benchmark 测试脚本来测试速度!对计算图应用一些 Relay pass,从而利用稀疏。 + +首先用 simple_fc_transpose 将密集层的权重转置到参数中,便于矩阵乘法转换为稀疏版本。接下来应用 bsr_dense.convert 来识别所有可以稀疏的权重矩阵,并自动替换它们。 + +下面的 bsr_dense.convert 函数通过检查 sparse_threshold 百分比稀疏,识别模型中的哪些权重可以变得稀疏,并将这些权重转换为 *Block Compressed Row Format (BSR)*。 + +BSR 本质上是一种对张量的 nonzero chunks 进行索引的表示,使算法可以轻松加载那些 nonzero chunks,并忽略张量的其余部分。一旦稀疏权重采用 BSR 格式,就会应用 relay.transform.DenseToSparse,实际上是用 relay.sparse_dense 函数来替换 relay.dense 操作,从而运行更快。 + +``` python +def random_bsr_matrix(M, N, BS_R, BS_C, density, dtype="float32"): + Y = np.zeros((M, N), dtype=dtype) + assert M % BS_R == 0 + assert N % BS_C == 0 + nnz = int(density * M * N) + num_blocks = int(nnz / (BS_R * BS_C)) + 1 + candidate_blocks = np.asarray(list(itertools.product(range(0, M, BS_R), range(0, N, BS_C)))) + assert candidate_blocks.shape[0] == M // BS_R * N // BS_C + chosen_blocks = candidate_blocks[ + np.random.choice(candidate_blocks.shape[0], size=num_blocks, replace=False) + ] + for i in range(len(chosen_blocks)): + r, c = chosen_blocks[i] + Y[r : r + BS_R, c : c + BS_C] = np.random.uniform(-0.1, 0.1, (BS_R, BS_C)) + s = sp.bsr_matrix(Y, blocksize=(BS_R, BS_C)) + assert s.data.shape == (num_blocks, BS_R, BS_C) + assert s.data.size >= nnz + assert s.indices.shape == (num_blocks,) + assert s.indptr.shape == (M // BS_R + 1,) + return s.todense() + +def random_sparse_bert_params(func, params, density, BS_R, BS_C): + def deepcopy(param_dic): + ret = {} + for k, v in param_dic.items(): + ret[k] = tvm.nd.array(v.numpy()) + return ret + + new_params = deepcopy(params) + dense_weight_names = relay.analysis.sparse_dense._search_dense_op_weight(func) + for item in dense_weight_names: + name = str(item) + shape = new_params[name].shape + if shape[0] % BS_R == 0 and shape[1] % BS_C == 0: + new_w = random_bsr_matrix(shape[0], shape[1], BS_R, BS_C, density) + new_params[name] = tvm.nd.array(new_w) + return new_params + +def run_sparse(mod, params, shape_dict, target, dev, bs_r, sparsity, gen_weights): + mod, params = ddo.simplify_fc_transpose.convert(mod["main"], params) + if gen_weights: + params = random_sparse_bert_params(mod, params, BS_R=bs_r, BS_C=1, density=1 - sparsity) + mod, params = ddo.bsr_dense.convert(mod, params, (bs_r, 1), sparsity_threshold=0.8) + print("Block Sparse Model with {blocksize}x1 blocks:".format(blocksize=bs_r)) + return run_relay_graph(mod, params, shape_dict, target, dev) +``` + +## 运行所有代码 + +调用所有需要的函数,根据设置的参数对模型进行 benchmark 测试。注意,运行这个代码,首先需要取消最后一行的注释。 + +``` python +def benchmark(): + mod, params, shape_dict = import_graphdef(name, batch_size, seq_len) + run_dense(mod, params, shape_dict, target, dev) + if measure_sparse: + gen_weights = "prune" not in name + run_sparse(mod, params, shape_dict, target, dev, bs_r, sparsity, gen_weights) + +# benchmark() +``` + +## 样本输出 + +可参考下面在 AMD CPU 上运行的脚本输出,显示用稀疏模型可提高约 2.5 倍的速度。 + +``` bash +# Dense Model Benchmark: +# Cannot find config for target=llvm, workload=('dense_nopack.x86', ('TENSOR', (1, 768), 'float32'), ('TENSOR', (2, 768), 'float32'), None, 'float32'). A fallback configuration is used, which may bring great performance regression. +# Cannot find config for target=llvm, workload=('dense_nopack.x86', ('TENSOR', (1, 768), 'float32'), ('TENSOR', (768, 768), 'float32'), None, 'float32'). A fallback configuration is used, which may bring great performance regression. +# Cannot find config for target=llvm, workload=('dense_nopack.x86', ('TENSOR', (128, 3072), 'float32'), ('TENSOR', (768, 3072), 'float32'), None, 'float32'). A fallback configuration is used, which may bring great performance regression. +# Cannot find config for target=llvm, workload=('dense_nopack.x86', ('TENSOR', (128, 768), 'float32'), ('TENSOR', (3072, 768), 'float32'), None, 'float32'). A fallback configuration is used, which may bring great performance regression. +# Cannot find config for target=llvm, workload=('dense_nopack.x86', ('TENSOR', (128, 768), 'float32'), ('TENSOR', (768, 768), 'float32'), None, 'float32'). A fallback configuration is used, which may bring great performance regression. +# Cannot find config for target=llvm, workload=('batch_matmul.x86', ('TENSOR', (12, 128, 128), 'float32'), ('TENSOR', (12, 64, 128), 'float32')). A fallback configuration is used, which may bring great performance regression. +# Cannot find config for target=llvm, workload=('batch_matmul.x86', ('TENSOR', (12, 128, 64), 'float32'), ('TENSOR', (12, 128, 64), 'float32')). A fallback configuration is used, which may bring great performance regression. +# Runtime: 165.26 ms (12.83 ms) +# Block Sparse Model with 1x1 blocks: +# Runtime: 67.75 ms (8.83 ms) + +# Here is the output of this script on a GPU (GTX 1070) with the target "cuda -libs=cublas". +# +# Dense Model Benchmark: +# Cannot find config for target=cuda -keys=cuda,gpu -libs=cublas -max_num_threads=1024 -thread_warp_size=32, workload=('dense_cublas.cuda', ('TENSOR', (1, 768), 'float32'), ('TENSOR', (2, 768), 'float32'), None, 'float32'). A fallback configuration is used, which may bring great performance regression. +# Cannot find config for target=cuda -keys=cuda,gpu -libs=cublas -max_num_threads=1024 -thread_warp_size=32, workload=('dense_cublas.cuda', ('TENSOR', (1, 768), 'float32'), ('TENSOR', (768, 768), 'float32'), None, 'float32'). A fallback configuration is used, which may bring great performance regression. +# Cannot find config for target=cuda -keys=cuda,gpu -libs=cublas -max_num_threads=1024 -thread_warp_size=32, workload=('dense_cublas.cuda', ('TENSOR', (128, 3072), 'float32'), ('TENSOR', (768, 3072), 'float32'), None, 'float32'). A fallback configuration is used, which may bring great performance regression. +# Cannot find config for target=cuda -keys=cuda,gpu -libs=cublas -max_num_threads=1024 -thread_warp_size=32, workload=('dense_cublas.cuda', ('TENSOR', (128, 768), 'float32'), ('TENSOR', (3072, 768), 'float32'), None, 'float32'). A fallback configuration is used, which may bring great performance regression. +# Cannot find config for target=cuda -keys=cuda,gpu -libs=cublas -max_num_threads=1024 -thread_warp_size=32, workload=('dense_cublas.cuda', ('TENSOR', (128, 768), 'float32'), ('TENSOR', (768, 768), 'float32'), None, 'float32'). A fallback configuration is used, which may bring great performance regression. +# Cannot find config for target=cuda -keys=cuda,gpu -libs=cublas -max_num_threads=1024 -thread_warp_size=32, workload=('batch_matmul_cublas.cuda', ('TENSOR', (12, 128, 128), 'float32'), ('TENSOR', (12, 64, 128), 'float32'), (12, 128, 64)). A fallback configuration is used, which may bring great performance regression. +# Cannot find config for target=cuda -keys=cuda,gpu -libs=cublas -max_num_threads=1024 -thread_warp_size=32, workload=('batch_matmul_cublas.cuda', ('TENSOR', (12, 128, 64), 'float32'), ('TENSOR', (12, 128, 64), 'float32'), (12, 128, 128)). A fallback configuration is used, which may bring great performance regression. +# Runtime: 10.64 ms (0.29 ms) +# Block Sparse Model with 1x1 blocks: +# Runtime: 6.46 ms (0.05 ms) +``` + +[下载 Python 源代码:deploy_sparse.py](https://tvm.apache.org/docs/_downloads/9c3764c88ab3eb57dc223b4eda1e8a2f/deploy_sparse.py) + +[下载 Jupyter Notebook:deploy_sparse.ipynb](https://tvm.apache.org/docs/_downloads/0b60295044fd20226a0d5adc52b50b2f/deploy_sparse.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/deploy/deploy_models/09-deploy_ssd.md b/versioned_docs/version-0.12.0/how_to/deploy/deploy_models/09-deploy_ssd.md new file mode 100644 index 00000000..eee3d9df --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/deploy/deploy_models/09-deploy_ssd.md @@ -0,0 +1,162 @@ +--- +title: 部署 Single Shot Multibox Detector(SSD)模型 +--- + +# 部署 Single Shot Multibox Detector(SSD)模型 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/deploy_models/deploy_ssd_gluoncv.html#sphx-glr-download-how-to-deploy-models-deploy-ssd-gluoncv-py) 下载完整的示例代码 +::: + +**作者**:[Yao Wang](https://github.com/kevinthesun),[Leyuan Wang](https://github.com/Laurawly) + +本文介绍如何用 TVM 部署 SSD 模型。这里将使用 GluonCV 预训练的 SSD 模型,并将其转换为 Relay IR。 + +``` python +import tvm +from tvm import te + +from matplotlib import pyplot as plt +from tvm import relay +from tvm.contrib import graph_executor +from tvm.contrib.download import download_testdata +from gluoncv import model_zoo, data, utils +``` + +输出结果: + +``` bash +/usr/local/lib/python3.7/dist-packages/gluoncv/__init__.py:40: UserWarning: Both `mxnet==1.6.0` and `torch==1.11.0+cpu` are installed. You might encounter increased GPU memory footprint if both framework are used at the same time. + warnings.warn(f'Both `mxnet=={mx.__version__}` and `torch=={torch.__version__}` are installed. ' +``` + +## 初步参数设置 + +:::note +现在支持在 CPU 和 GPU 上编译 SSD。 + +为取得 CPU 上的最佳推理性能,需要根据设备修改 target 参数——对于 x86 CPU:参考 [为 x86 CPU 自动调整卷积网络](/docs/how_to/autotune/autotuning_x86) 来调整;对于 arm CPU:参考 [为 ARM CPU 自动调整卷积网络](/docs/how_to/autotune/autotuning_arm) 来调整。 + +为在 Intel 显卡上取得最佳推理性能,将 target 参数修改为 `opencl -device=intel_graphics` 。注意:在 Mac 上使用 Intel 显卡时,target 要设置为 `opencl` ,因为 Mac 上不支持 Intel 子组扩展。 + +为取得基于 CUDA 的 GPU 上的最佳推理性能,将 target 参数修改为 `cuda`;对于基于 OPENCL 的 GPU,将 target 参数修改为 `opencl`,然后根据设备来修改设备参数。 +::: + +``` python +supported_model = [ + "ssd_512_resnet50_v1_voc", + "ssd_512_resnet50_v1_coco", + "ssd_512_resnet101_v2_voc", + "ssd_512_mobilenet1.0_voc", + "ssd_512_mobilenet1.0_coco", + "ssd_300_vgg16_atrous_voc" "ssd_512_vgg16_atrous_coco", +] + +model_name = supported_model[0] +dshape = (1, 3, 512, 512) +``` + +下载并预处理 demo 图像: + +``` python +im_fname = download_testdata( + "https://github.com/dmlc/web-data/blob/main/" + "gluoncv/detection/street_small.jpg?raw=true", + "street_small.jpg", + module="data", +) +x, img = data.transforms.presets.ssd.load_test(im_fname, short=512) +``` + +为 CPU 转换和编译模型: + +``` python +block = model_zoo.get_model(model_name, pretrained=True) + +def build(target): + mod, params = relay.frontend.from_mxnet(block, {"data": dshape}) + with tvm.transform.PassContext(opt_level=3): + lib = relay.build(mod, target, params=params) + return lib +``` + +输出结果: + +``` bash +/usr/local/lib/python3.7/dist-packages/mxnet/gluon/block.py:1389: UserWarning: Cannot decide type for the following arguments. Consider providing them as input: + data: None + input_sym_arg_type = in_param.infer_type()[0] +Downloading /workspace/.mxnet/models/ssd_512_resnet50_v1_voc-9c8b225a.zip from https://apache-mxnet.s3-accelerate.dualstack.amazonaws.com/gluon/models/ssd_512_resnet50_v1_voc-9c8b225a.zip... + + 0%| | 0/132723 [00:00 () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {a: Buffer(a_2: Pointer(float32), float32, [128], []), + b: Buffer(b_2: Pointer(float32), float32, [128], []), + c: Buffer(c_2: Pointer(float32), float32, [128], [])} + buffer_map = {a_1: a, b_1: b, c_1: c} + preflattened_buffer_map = {a_1: a_3: Buffer(a_2, float32, [128], []), b_1: b_3: Buffer(b_2, float32, [128], []), c_1: c_3: Buffer(c_2, float32, [128], [])} { + for (i: int32, 0, 128) { + c[i] = (a[i] + b[i]) + } +} +``` + +## 编写 Pass + +本质上,「IR 转换 pass」是将语句映射到新语句的函数。因此,我们要定义这个向量化函数,并逐步实现它。 + +TVM 为用户提供了两个类来分析和转换 IR。 + +### IR 访问器 + +可以用 `tvm.tir.stmt_functor.post_order_visit(stmt, func)` 从 Halide IR 中收集信息。 `func` 是一个回调函数,会在退出当前 IR 节点之前调用,即 post-order visit。然后存储 IR 访问的结果,因为 `func` 的返回值将被忽略。 + +:::note +必须用数组来存储 IR 访问的结果。值甚至是一个单变量。这主要是由于 Python-C runtime 的限制,每次递归都会刷新变量值,但会保留数组值。 +::: + +``` python +loops = [] + +def find_width8(op): + """查找范围可以被 8 整除的所有「tir.For」节点。""" + if isinstance(op, tvm.tir.For): + if isinstance(op.extent, tvm.tir.IntImm): + if op.extent.value % 8 == 0: + loops.append(op) +``` + +### IR 转换 + +转换接口与访问器接口略有不同。访问器中只有一个后序回调,但转换访问器同时支持前序回调和后序回调。若要保留原始 IR 节点,只需返回 None。若要将当前节点更改为某个节点,使用 TVM IR maker 接口构建,并返回这个值。 + +:::note +若调用 pre-order 函数后返回一个非 None 的值,则将跳过 post-order 函数。 +::: + +``` python +def vectorize8(op): + """Split 可以向量化 `find_width8` 中的循环。""" + if op in loops: + extent = op.extent.value + name = op.loop_var.name + lo, li = te.var(name + ".outer"), te.var(name + ".inner") + body = tvm.tir.stmt_functor.substitute(op.body, {op.loop_var: lo * 8 + li}) + body = tvm.tir.For(li, 0, 8, tvm.tir.ForKind.VECTORIZED, body) + body = tvm.tir.For(lo, 0, extent // 8, tvm.tir.ForKind.SERIAL, body) + return body + return None + +@tvm.tir.transform.prim_func_pass(opt_level=0) +def vectorize(f, mod, ctx): + global loops + + tvm.tir.stmt_functor.post_order_visit(f.body, find_width8) + + if not loops: + return f + + # 最后一个列表参数表示将转换哪些类型的节点。 + # 在这种情况下,只有 `For` 节点会调用 `vectorize8` + return f.with_body(tvm.tir.stmt_functor.ir_transform(f.body, None, vectorize8, ["tir.For"])) +``` + +## 对接低层(Glue to Lowering) + +到目前为止,已经完成了这个 IR 转换 pass 的编写。接下来将这个 pass 和 TVM 的底层 pass 对接。 + +在这种情况下,通过**元组列表**作为参数提供给 `tir.add_lower_pass`,将上面编写的 pass 注入 TVM 标准较低级的 pass。「元组」表示降级的不同阶段。 TVM 中有四个阶段的降级,每个阶段完成后,都会调用自定义的阶段。 + +:::note +**以下是每个阶段完成的基本转换:** + +* 阶段 0 生成原始 IR 和循环级别。 +* 阶段 1 扁平化数组存储。 +* 阶段 2 转换循环,如展开、矢量化和线程绑定。 +* 阶段 3 清理工作。 +::: + +因此,这个转换 pass 适合放在第 1 阶段之后。 + +```python +with tvm.transform.PassContext(config={"tir.add_lower_pass": [(1, vectorize)]}): + print(tvm.lower(sch, [a, b, c])) +``` + +输出结果: + +``` bash +@main = primfn(a_1: handle, b_1: handle, c_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {a: Buffer(a_2: Pointer(float32), float32, [128], []), + b: Buffer(b_2: Pointer(float32), float32, [128], []), + c: Buffer(c_2: Pointer(float32), float32, [128], [])} + buffer_map = {a_1: a, b_1: b, c_1: c} + preflattened_buffer_map = {a_1: a_3: Buffer(a_2, float32, [128], []), b_1: b_3: Buffer(b_2, float32, [128], []), c_1: c_3: Buffer(c_2, float32, [128], [])} { + for (i.outer: int32, 0, 16) { + let cse_var_1: int32 = (i.outer*8) + c[ramp(cse_var_1, 1, 8)] = (a[ramp(cse_var_1, 1, 8)] + b[ramp(cse_var_1, 1, 8)]) + } +} +``` + +## 快速回顾 + +快速回顾本教程有关编写自定义 IR 转换 pass: + +* 用 `tvm.tir.stmt_functor.post_order_visit` 收集每个 IR 节点的信息。 +* 用 `tvm.tir.stmt_functor.ir_transform` 转换 IR 节点。 +* 总结以上两点来编写一个 IR 转换函数。 +* 用 `tvm.transform.PassContext` 将此函数放入 TVM 降级 pass。 + +[下载 Python 源代码:low_level_custom_pass.py](https://tvm.apache.org/docs/_downloads/caa649473e845a115a0397a2855fd356/low_level_custom_pass.py) + +[下载 Jupyter Notebook:low_level_custom_pass.ipynb](https://tvm.apache.org/docs/_downloads/d58ec306b89044968adefb49e6552378/low_level_custom_pass.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/extend/02-pass_infra.md b/versioned_docs/version-0.12.0/how_to/extend/02-pass_infra.md new file mode 100644 index 00000000..2dc50bb2 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/extend/02-pass_infra.md @@ -0,0 +1,446 @@ +--- +title: 如何使用 TVM Pass Infra +--- + +# 如何使用 TVM Pass Infra + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/extend_tvm/use_pass_infra.html#sphx-glr-download-how-to-extend-tvm-use-pass-infra-py) 下载完整的示例代码 +::: + +**作者**:[Zhi Chen](https://github.com/zhiics) + +随着 Relay/tir 中优化 Pass 数的增加,手动执行并维护它们的依赖关系变得难以处理。因此我们引入了一个基础架构来管理优化 Pass,并使其适用于 TVM 堆栈中 IR 的不同层。 + +Relay/tir 程序的优化可以在各种粒度上应用,即分别使用 `tvm.relay.transform.FunctionPass`/ `tvm.tir.transform.PrimFuncPass` 和 `tvm.transform.ModulePass` 的功能级和模块级,或者用户可以依靠 `tvm.transform.Sequential` 在 Relay/tir 程序上应用一系列 Pass,其中 Pass 之间的依赖关系可以通过 pass infra 来解决。有关这些 Pass 类型的更多详细信息,参阅 [Pass Infrastructure](https://tvm.apache.org/docs/arch/pass_infra.html#pass-infra)。 + +本教程主要演示开发者如何使用 pass infra 进行某种优化,并为 Relay 程序创建优化 Pass。同样的方法也可以用于 tir。 + +``` python +import numpy as np +import tvm +from tvm import te +import tvm.relay as relay +``` + +## 创建 Relay 程序示例 + +首先为教程创建一个简单的 Relay 程序,该程序用于本教程中示例的各种优化。同样,用户可以编写 tir 原始函数并应用 tir pass。 + +``` python +def example(): + shape = (1, 64, 54, 54) + c_data = np.empty(shape).astype("float32") + c = relay.const(c_data) + weight = relay.var("weight", shape=(64, 64, 3, 3)) + x = relay.var("x", relay.TensorType((1, 64, 56, 56), "float32")) + conv = relay.nn.conv2d(x, weight) + y = relay.add(c, c) + y = relay.multiply(y, relay.const(2, "float32")) + y = relay.add(conv, y) + z = relay.add(y, c) + z1 = relay.add(y, c) + z2 = relay.add(z, z1) + return relay.Function([x, weight], z2) +``` + +## 优化程序 + +接下来优化程序,Relay 具有许多优化功能,选择其中一部分应用到这个示例程序中。 + +有多种方法可以优化 Relay 程序。下面将逐一讲解。 + +### 手动应用优化 Pass + +``` python +# 首先创建一个包含一个或多个 Relay 的 relay 模块 +# 优化函数。 +f = example() +mod = tvm.IRModule.from_expr(f) + +# 现在可以在模块上应用常量折叠。 +# fold_const 是一个不带任何参数的回调函数。 +fold_const = relay.transform.FoldConstant() +# 然后,在给定的模块上调用 pass。注意,常数 +# folding pass 在函数级别工作。话虽如此,每个 +# 模块中的函数将应用优化。用户无需迭代 +# 通过各个函数手动应用此 pass。 +mod = fold_const(mod) +# 从更新后的程序中可以看到常量被折叠了。 +print(mod) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +def @main(%x: Tensor[(1, 64, 56, 56), float32] /* ty=Tensor[(1, 64, 56, 56), float32] */, %weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */) -> Tensor[(1, 64, 54, 54), float32] { + %0 = nn.conv2d(%x, %weight, padding=[0, 0, 0, 0]) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %1 = add(%0, meta[relay.Constant][0] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %2 = add(%1, meta[relay.Constant][1] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %3 = add(%1, meta[relay.Constant][1] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + add(%2, %3) /* ty=Tensor[(1, 64, 54, 54), float32] */ +} +``` + +可以以类似的方式应用更多优化。例如,可以消除 *z* 和 *z1* 使用的常用表达式。 + +``` python +mod = relay.transform.EliminateCommonSubexpr()(mod) +print(mod) +``` + +输出结果: + +``` bash +def @main(%x: Tensor[(1, 64, 56, 56), float32] /* ty=Tensor[(1, 64, 56, 56), float32] */, %weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */) -> Tensor[(1, 64, 54, 54), float32] { + %0 = nn.conv2d(%x, %weight, padding=[0, 0, 0, 0]) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %1 = add(%0, meta[relay.Constant][0] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %2 = add(%1, meta[relay.Constant][1] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + add(%2, %2) /* ty=Tensor[(1, 64, 54, 54), float32] */ +} +``` + +融合也是参数化的,例如,opt level 0 将不允许算子融合在一起。用户可以通过 fuse_opt_level 来启用它。 + +``` python +mod = relay.transform.FuseOps(fuse_opt_level=0)(mod) + +# 可以观察到优化后的模块包含的函数只有 +# 一个单一的原始操作。 +print(mod) +``` + +输出结果: + +``` bash +def @main(%x: Tensor[(1, 64, 56, 56), float32] /* ty=Tensor[(1, 64, 56, 56), float32] */, %weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */) -> Tensor[(1, 64, 54, 54), float32] { + %0 = fn (%p03: Tensor[(1, 64, 56, 56), float32] /* ty=Tensor[(1, 64, 56, 56), float32] */, %p12: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */, Primitive=1) -> Tensor[(1, 64, 54, 54), float32] { + nn.conv2d(%p03, %p12, padding=[0, 0, 0, 0]) /* ty=Tensor[(1, 64, 54, 54), float32] */ + } /* ty=fn (Tensor[(1, 64, 56, 56), float32], Tensor[(64, 64, 3, 3), float32]) -> Tensor[(1, 64, 54, 54), float32] */; + %1 = %0(%x, %weight) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %2 = fn (%p02: Tensor[(1, 64, 54, 54), float32] /* ty=Tensor[(1, 64, 54, 54), float32] */, %p11: Tensor[(1, 64, 54, 54), float32] /* ty=Tensor[(1, 64, 54, 54), float32] */, Primitive=1) -> Tensor[(1, 64, 54, 54), float32] { + add(%p02, %p11) /* ty=Tensor[(1, 64, 54, 54), float32] */ + } /* ty=fn (Tensor[(1, 64, 54, 54), float32], Tensor[(1, 64, 54, 54), float32]) -> Tensor[(1, 64, 54, 54), float32] */; + %3 = %2(%1, meta[relay.Constant][0] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %4 = fn (%p01: Tensor[(1, 64, 54, 54), float32] /* ty=Tensor[(1, 64, 54, 54), float32] */, %p1: Tensor[(1, 64, 54, 54), float32] /* ty=Tensor[(1, 64, 54, 54), float32] */, Primitive=1) -> Tensor[(1, 64, 54, 54), float32] { + add(%p01, %p1) /* ty=Tensor[(1, 64, 54, 54), float32] */ + } /* ty=fn (Tensor[(1, 64, 54, 54), float32], Tensor[(1, 64, 54, 54), float32]) -> Tensor[(1, 64, 54, 54), float32] */; + %5 = %4(%3, meta[relay.Constant][1] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %6 = fn (%p0: Tensor[(1, 64, 54, 54), float32] /* ty=Tensor[(1, 64, 54, 54), float32] */, Primitive=1) -> Tensor[(1, 64, 54, 54), float32] { + add(%p0, %p0) /* ty=Tensor[(1, 64, 54, 54), float32] */ + } /* ty=fn (Tensor[(1, 64, 54, 54), float32]) -> Tensor[(1, 64, 54, 54), float32] */; + %6(%5) /* ty=Tensor[(1, 64, 54, 54), float32] */ +} +``` + +### 使用 Sequential 应用一系列 Pass + +如上所述应用 Pass 实际上很乏味,并且可能需要用户更好地了解它们之间的依赖关系。例如,融合目前在 let 绑定上效果不佳。因此,如果在融合之前应用了 `relay.transform.ToANormalForm()`,将无法融合可融合的算子,因为此过程会为每个表达式生成 let 绑定以规范 Relay 程序。 + +因此,Relay 提供了 `tvm.transform.Sequential`,使得开发者能够更容易地处理这些问题。他们通过显式指定每个 pass 所需的 pass,然后将它们打包为一个整体来实现。 + +例如,使用下面的 sequential 来应用相同的 pass。`tvm.transform.Sequential` 类似于 [torch.nn.sequential](https://pytorch.org/docs/stable/nn.html#torch.nn.Sequential) 和 [mxnet.gluon.block](https://mxnet.apache.org/api/python/docs/_modules/mxnet/gluon/block.html)。例如,torch.nn.sequential 包含一系列 PyTorch 模块,这些模块将会用来构建网络。它侧重于网络层。相反,我们的 pass infra 中的 `tvm.transform.Sequential` 用于优化 pass。 + +``` python +# 通过 :py:class:`tvm.transform.Sequential` 执行一些传递 +f = example() +mod = tvm.IRModule.from_expr(f) +# Glob 感兴趣的 passes。 +seq = tvm.transform.Sequential( + [ + relay.transform.FoldConstant(), + relay.transform.EliminateCommonSubexpr(), + relay.transform.FuseOps(fuse_opt_level=2), + ] +) +mod1 = seq(mod) +print(mod1) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +def @main(%x: Tensor[(1, 64, 56, 56), float32] /* ty=Tensor[(1, 64, 56, 56), float32] */, %weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */) -> Tensor[(1, 64, 54, 54), float32] { + %4 = fn (%p0: Tensor[(1, 64, 56, 56), float32] /* ty=Tensor[(1, 64, 56, 56), float32] */, %p1: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */, %p2: Tensor[(1, 64, 54, 54), float32] /* ty=Tensor[(1, 64, 54, 54), float32] */, %p3: Tensor[(1, 64, 54, 54), float32] /* ty=Tensor[(1, 64, 54, 54), float32] */, Primitive=1) -> Tensor[(1, 64, 54, 54), float32] { + %0 = nn.conv2d(%p0, %p1, padding=[0, 0, 0, 0]) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %1 = add(%0, %p2) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %2 = add(%1, %p3) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %3 = add(%1, %p3) /* ty=Tensor[(1, 64, 54, 54), float32] */; + add(%2, %3) /* ty=Tensor[(1, 64, 54, 54), float32] */ + } /* ty=fn (Tensor[(1, 64, 56, 56), float32], Tensor[(64, 64, 3, 3), float32], Tensor[(1, 64, 54, 54), float32], Tensor[(1, 64, 54, 54), float32]) -> Tensor[(1, 64, 54, 54), float32] */; + %4(%x, %weight, meta[relay.Constant][0] /* ty=Tensor[(1, 64, 54, 54), float32] */, meta[relay.Constant][1] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */ +} +``` + +从改造后的 Relay 程序中,可以看到仍然有两个相同的加法运算。这是因为 `EliminateCommonSubexpr` 并未实际执行。原因是在 `tvm.transform.Sequential` 下,只有优化级别小于或等于 2 的 pass 才会默认执行。pass infra 为用户提供了一个配置界面来自定义想要执行的优化级别。 + +``` python +with tvm.transform.PassContext(opt_level=3): + mod2 = seq(mod) +print(mod2) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +def @main(%x: Tensor[(1, 64, 56, 56), float32] /* ty=Tensor[(1, 64, 56, 56), float32] */, %weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */) -> Tensor[(1, 64, 54, 54), float32] { + %3 = fn (%p0: Tensor[(1, 64, 56, 56), float32] /* ty=Tensor[(1, 64, 56, 56), float32] */, %p1: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */, %p2: Tensor[(1, 64, 54, 54), float32] /* ty=Tensor[(1, 64, 54, 54), float32] */, %p3: Tensor[(1, 64, 54, 54), float32] /* ty=Tensor[(1, 64, 54, 54), float32] */, Primitive=1) -> Tensor[(1, 64, 54, 54), float32] { + %0 = nn.conv2d(%p0, %p1, padding=[0, 0, 0, 0]) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %1 = add(%0, %p2) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %2 = add(%1, %p3) /* ty=Tensor[(1, 64, 54, 54), float32] */; + add(%2, %2) /* ty=Tensor[(1, 64, 54, 54), float32] */ + } /* ty=fn (Tensor[(1, 64, 56, 56), float32], Tensor[(64, 64, 3, 3), float32], Tensor[(1, 64, 54, 54), float32], Tensor[(1, 64, 54, 54), float32]) -> Tensor[(1, 64, 54, 54), float32] */; + %3(%x, %weight, meta[relay.Constant][0] /* ty=Tensor[(1, 64, 54, 54), float32] */, meta[relay.Constant][1] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */ +} +``` + +现在可以看到两个相同的加法只保留一个。 + +用户可以使用 *disabled_pass* 配置选择性地禁用某些 pass,这与 Clang 和 GCC 等通用编译器使用的 *-fno-xxx* 选项类似。例如,可以禁用以下 EliminateCommonSubexpr,打印的模块将再次显示两个相同的加法操作。 + +``` python +with tvm.transform.PassContext(opt_level=3, disabled_pass=["EliminateCommonSubexpr"]): + mod3 = seq(mod) +print(mod3) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +def @main(%x: Tensor[(1, 64, 56, 56), float32] /* ty=Tensor[(1, 64, 56, 56), float32] */, %weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */) -> Tensor[(1, 64, 54, 54), float32] { + %4 = fn (%p0: Tensor[(1, 64, 56, 56), float32] /* ty=Tensor[(1, 64, 56, 56), float32] */, %p1: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */, %p2: Tensor[(1, 64, 54, 54), float32] /* ty=Tensor[(1, 64, 54, 54), float32] */, %p3: Tensor[(1, 64, 54, 54), float32] /* ty=Tensor[(1, 64, 54, 54), float32] */, Primitive=1) -> Tensor[(1, 64, 54, 54), float32] { + %0 = nn.conv2d(%p0, %p1, padding=[0, 0, 0, 0]) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %1 = add(%0, %p2) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %2 = add(%1, %p3) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %3 = add(%1, %p3) /* ty=Tensor[(1, 64, 54, 54), float32] */; + add(%2, %3) /* ty=Tensor[(1, 64, 54, 54), float32] */ + } /* ty=fn (Tensor[(1, 64, 56, 56), float32], Tensor[(64, 64, 3, 3), float32], Tensor[(1, 64, 54, 54), float32], Tensor[(1, 64, 54, 54), float32]) -> Tensor[(1, 64, 54, 54), float32] */; + %4(%x, %weight, meta[relay.Constant][0] /* ty=Tensor[(1, 64, 54, 54), float32] */, meta[relay.Constant][1] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */ +} +``` + +## 使用 Python 装饰器实现 Pass + +下一个示例说明如何使用 Python 装饰器通过 pass infra 来自定义优化 pipeline。此功能极大地简化了pass 的实现。例如,用户可以简单地定义一个装饰类来进行函数级优化,如下例所示。 *transform_function* 包装了一个类,用 c 的倍数替换所有常量。稍后,访问给定模块中的每个函数,并且调用自定义 pass 时,函数中的每个常量都将被替换。 + +``` python +@relay.transform.function_pass(opt_level=1) +class CustomPipeline: + """Simple test function to replace one argument to another.""" + + def __init__(self, multiplier): + self.multiplier = multiplier + + # 这个函数可以定义一个pass。 + def transform_function(self, func, mod, ctx): + obj = self + + class ReplaceConstant(tvm.relay.ExprMutator): + def visit_constant(self, c): + return relay.multiply(obj.multiplier, c) + + return ReplaceConstant().visit(func) + +f = example() +mod = tvm.IRModule.from_expr(f) +custom_pass = CustomPipeline(multiplier=relay.const(3, "float32")) +assert custom_pass.info.name == "CustomPipeline" +mod3 = custom_pass(mod) +print(mod3) +``` + +输出结果: + +``` bash +def @main(%x: Tensor[(1, 64, 56, 56), float32] /* ty=Tensor[(1, 64, 56, 56), float32] */, %weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */) -> Tensor[(1, 64, 54, 54), float32] { + %0 = multiply(3f /* ty=float32 */, meta[relay.Constant][0] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %1 = add(%0, %0) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %2 = multiply(3f /* ty=float32 */, 2f /* ty=float32 */) /* ty=float32 */; + %3 = nn.conv2d(%x, %weight, padding=[0, 0, 0, 0]) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %4 = multiply(%1, %2) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %5 = add(%3, %4) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %6 = add(%5, %0) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %7 = add(%5, %0) /* ty=Tensor[(1, 64, 54, 54), float32] */; + add(%6, %7) /* ty=Tensor[(1, 64, 54, 54), float32] */ +} +``` + +## 调试 Pass + +TVM 为用户提供了即插即用式调试 Pass,通过特殊 pass (`PrintIR`) 转储整个模块的 IR,在完成某个 pass 后打印 IR。pass 序列示例的轻微修改版本如下所示,用于启用 IR 转储以进行 `FoldConstant` 优化。 + +``` python +f = example() +mod = tvm.IRModule.from_expr(f) +seq = tvm.transform.Sequential( + [ + relay.transform.FoldConstant(), + tvm.transform.PrintIR(), + relay.transform.EliminateCommonSubexpr(), + relay.transform.FuseOps(), + ] +) +``` + +通过在 `FoldConstant` 之后插入 `PrintIR` pass,pass infra 将在 `FoldConstant` 完成时转储模块 IR。用户可以在任何想要调试的 pass 之后插入这个 pass 来查看优化效果。 + +此外,还有一个更灵活的调试机制,可以实现一个 `PassInstrument` 类来执行任意代码,不仅在每次传递之前和/或之后,而且在进入/退出 `PassContext` 时也可以。有关详细信息,参阅 [Pass Instrument](https://tvm.apache.org/docs/arch/pass_infra.html#pass-instrument-cpp-backend)。 + +这里使用 `tvm.instrument.pass_instrument` 装饰器来实现一个 PassInsturment 类,在每次执行之前打印 IR: + +``` python +@tvm.instrument.pass_instrument +class PrintIR: + """仅在 pass 执行之前打印 pass 的名称,IR。""" + + def run_before_pass(self, mod, info): + print("Running pass: {}", info) + print(mod) + +with tvm.transform.PassContext(opt_level=3, instruments=[PrintIR()]): + with tvm.target.Target("llvm"): + # 执行优化。 + mod = seq(mod) +print(mod) + +print("done") +``` + +输出结果: + +``` bash +Running pass: {} The meta data of the pass - pass name: sequential, opt_level: 0, required passes: [] + +def @main(%x: Tensor[(1, 64, 56, 56), float32], %weight: Tensor[(64, 64, 3, 3), float32]) { + %0 = add(meta[relay.Constant][0], meta[relay.Constant][0]); + %1 = nn.conv2d(%x, %weight, padding=[0, 0, 0, 0]); + %2 = multiply(%0, 2f); + %3 = add(%1, %2); + %4 = add(%3, meta[relay.Constant][0]); + %5 = add(%3, meta[relay.Constant][0]); + add(%4, %5) +} + +Running pass: {} The meta data of the pass - pass name: FoldConstant, opt_level: 2, required passes: [] + +def @main(%x: Tensor[(1, 64, 56, 56), float32], %weight: Tensor[(64, 64, 3, 3), float32]) { + %0 = add(meta[relay.Constant][0], meta[relay.Constant][0]); + %1 = nn.conv2d(%x, %weight, padding=[0, 0, 0, 0]); + %2 = multiply(%0, 2f); + %3 = add(%1, %2); + %4 = add(%3, meta[relay.Constant][0]); + %5 = add(%3, meta[relay.Constant][0]); + add(%4, %5) +} + +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +Running pass: {} The meta data of the pass - pass name: InferType, opt_level: 0, required passes: [] + +def @main(%x: Tensor[(1, 64, 56, 56), float32], %weight: Tensor[(64, 64, 3, 3), float32]) { + %0 = nn.conv2d(%x, %weight, padding=[0, 0, 0, 0]); + %1 = add(%0, meta[relay.Constant][0]); + %2 = add(%1, meta[relay.Constant][1]); + %3 = add(%1, meta[relay.Constant][1]); + add(%2, %3) +} + +Running pass: {} The meta data of the pass - pass name: PrintIR, opt_level: 0, required passes: [] + +def @main(%x: Tensor[(1, 64, 56, 56), float32] /* ty=Tensor[(1, 64, 56, 56), float32] */, %weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */) -> Tensor[(1, 64, 54, 54), float32] { + %0 = nn.conv2d(%x, %weight, padding=[0, 0, 0, 0]) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %1 = add(%0, meta[relay.Constant][0] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %2 = add(%1, meta[relay.Constant][1] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %3 = add(%1, meta[relay.Constant][1] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + add(%2, %3) /* ty=Tensor[(1, 64, 54, 54), float32] */ +} + +Running pass: {} The meta data of the pass - pass name: InferType, opt_level: 0, required passes: [] + +def @main(%x: Tensor[(1, 64, 56, 56), float32] /* ty=Tensor[(1, 64, 56, 56), float32] */, %weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */) -> Tensor[(1, 64, 54, 54), float32] { + %0 = nn.conv2d(%x, %weight, padding=[0, 0, 0, 0]) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %1 = add(%0, meta[relay.Constant][0] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %2 = add(%1, meta[relay.Constant][1] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %3 = add(%1, meta[relay.Constant][1] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + add(%2, %3) /* ty=Tensor[(1, 64, 54, 54), float32] */ +} + +Running pass: {} The meta data of the pass - pass name: EliminateCommonSubexpr, opt_level: 3, required passes: [ +InferType, ] + +def @main(%x: Tensor[(1, 64, 56, 56), float32] /* ty=Tensor[(1, 64, 56, 56), float32] */, %weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */) -> Tensor[(1, 64, 54, 54), float32] { + %0 = nn.conv2d(%x, %weight, padding=[0, 0, 0, 0]) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %1 = add(%0, meta[relay.Constant][0] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %2 = add(%1, meta[relay.Constant][1] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %3 = add(%1, meta[relay.Constant][1] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + add(%2, %3) /* ty=Tensor[(1, 64, 54, 54), float32] */ +} + +Running pass: {} The meta data of the pass - pass name: InferType, opt_level: 0, required passes: [] + +def @main(%x: Tensor[(1, 64, 56, 56), float32] /* ty=Tensor[(1, 64, 56, 56), float32] */, %weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */) -> Tensor[(1, 64, 54, 54), float32] { + %0 = nn.conv2d(%x, %weight, padding=[0, 0, 0, 0]) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %1 = add(%0, meta[relay.Constant][0] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %2 = add(%1, meta[relay.Constant][1] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + add(%2, %2) /* ty=Tensor[(1, 64, 54, 54), float32] */ +} + +Running pass: {} The meta data of the pass - pass name: InferType, opt_level: 0, required passes: [] + +def @main(%x: Tensor[(1, 64, 56, 56), float32] /* ty=Tensor[(1, 64, 56, 56), float32] */, %weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */) -> Tensor[(1, 64, 54, 54), float32] { + %0 = nn.conv2d(%x, %weight, padding=[0, 0, 0, 0]) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %1 = add(%0, meta[relay.Constant][0] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %2 = add(%1, meta[relay.Constant][1] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + add(%2, %2) /* ty=Tensor[(1, 64, 54, 54), float32] */ +} + +Running pass: {} The meta data of the pass - pass name: FuseOps, opt_level: 0, required passes: [ +InferType, ] + +def @main(%x: Tensor[(1, 64, 56, 56), float32] /* ty=Tensor[(1, 64, 56, 56), float32] */, %weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */) -> Tensor[(1, 64, 54, 54), float32] { + %0 = nn.conv2d(%x, %weight, padding=[0, 0, 0, 0]) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %1 = add(%0, meta[relay.Constant][0] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %2 = add(%1, meta[relay.Constant][1] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */; + add(%2, %2) /* ty=Tensor[(1, 64, 54, 54), float32] */ +} + +Running pass: {} The meta data of the pass - pass name: InferType, opt_level: 0, required passes: [] + +def @main(%x: Tensor[(1, 64, 56, 56), float32] /* ty=Tensor[(1, 64, 56, 56), float32] */, %weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */) -> Tensor[(1, 64, 54, 54), float32] { + %3 = fn (%p0: Tensor[(1, 64, 56, 56), float32], %p1: Tensor[(64, 64, 3, 3), float32], %p2: Tensor[(1, 64, 54, 54), float32], %p3: Tensor[(1, 64, 54, 54), float32], Primitive=1) -> Tensor[(1, 64, 54, 54), float32] { + %0 = nn.conv2d(%p0, %p1, padding=[0, 0, 0, 0]); + %1 = add(%0, %p2); + %2 = add(%1, %p3); + add(%2, %2) + }; + %3(%x, %weight, meta[relay.Constant][0] /* ty=Tensor[(1, 64, 54, 54), float32] */, meta[relay.Constant][1] /* ty=Tensor[(1, 64, 54, 54), float32] */) +} + +def @main(%x: Tensor[(1, 64, 56, 56), float32] /* ty=Tensor[(1, 64, 56, 56), float32] */, %weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */) -> Tensor[(1, 64, 54, 54), float32] { + %3 = fn (%p0: Tensor[(1, 64, 56, 56), float32] /* ty=Tensor[(1, 64, 56, 56), float32] */, %p1: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */, %p2: Tensor[(1, 64, 54, 54), float32] /* ty=Tensor[(1, 64, 54, 54), float32] */, %p3: Tensor[(1, 64, 54, 54), float32] /* ty=Tensor[(1, 64, 54, 54), float32] */, Primitive=1) -> Tensor[(1, 64, 54, 54), float32] { + %0 = nn.conv2d(%p0, %p1, padding=[0, 0, 0, 0]) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %1 = add(%0, %p2) /* ty=Tensor[(1, 64, 54, 54), float32] */; + %2 = add(%1, %p3) /* ty=Tensor[(1, 64, 54, 54), float32] */; + add(%2, %2) /* ty=Tensor[(1, 64, 54, 54), float32] */ + } /* ty=fn (Tensor[(1, 64, 56, 56), float32], Tensor[(64, 64, 3, 3), float32], Tensor[(1, 64, 54, 54), float32], Tensor[(1, 64, 54, 54), float32]) -> Tensor[(1, 64, 54, 54), float32] */; + %3(%x, %weight, meta[relay.Constant][0] /* ty=Tensor[(1, 64, 54, 54), float32] */, meta[relay.Constant][1] /* ty=Tensor[(1, 64, 54, 54), float32] */) /* ty=Tensor[(1, 64, 54, 54), float32] */ +} + +done +``` + +## 总结 + +本教程介绍了如何使用 pass infra 更方便地在 TVM 中编写和调用 pass。还讨论了调用 pass 的不同方式。使用 `tvm.transform.Sequential` 可以在很大程度上帮助用户简化处理多个优化过程及其依赖关系的工作。此外,还提供了一个示例来说明如何使用 `PrintIR` 和跟踪来调试 pass。 + +[下载 Python 源代码:use_pass_infra.py](https://tvm.apache.org/docs/_downloads/5499fb7d70e17c6aabf49246a978db52/use_pass_infra.py) + +[下载 Jupyter Notebook:use_pass_infra.ipynb](https://tvm.apache.org/docs/_downloads/7ef14586a3b62fe120d97d5fedf72879/use_pass_infra.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/extend/03-pass_instrument.md b/versioned_docs/version-0.12.0/how_to/extend/03-pass_instrument.md new file mode 100644 index 00000000..0c911923 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/extend/03-pass_instrument.md @@ -0,0 +1,569 @@ +--- +title: 如何使用 TVM Pass Instrument +--- + +# 如何使用 TVM Pass Instrument + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/extend_tvm/use_pass_instrument.html#sphx-glr-download-how-to-extend-tvm-use-pass-instrument-py) 下载完整的示例代码 +::: + +**作者**:[Chi-Wei Wang](https://github.com/chiwwang) + +随着实现的 Pass 越来越多,instrument pass 执行、分析每个 Pass 效果和观察各种事件也愈发重要。 + +可以通过向 `tvm.transform.PassContext` 提供 `tvm.ir.instrument.PassInstrument` 实例列表来检测 Pass。我们提供了一个用于收集计时信息的 pass 工具(`tvm.ir.instrument.PassTimingInstrument`),可以通过 `tvm.instrument.pass_instrument()` 装饰器使用扩展机制。 + +本教程演示开发者如何用 `PassContext` 检测 Pass。另请参阅 [Pass Infrastructure](https://tvm.apache.org/docs/arch/pass_infra.html#pass-infra)。 + +``` python +import tvm +import tvm.relay as relay +from tvm.relay.testing import resnet +from tvm.contrib.download import download_testdata +from tvm.relay.build_module import bind_params_by_name +from tvm.ir.instrument import ( + PassTimingInstrument, + pass_instrument, +) +``` + +## 创建 Relay 程序示例 + +在 Relay 中使用预定义的 ResNet-18 网络。 + +``` python +batch_size = 1 +num_of_image_class = 1000 +image_shape = (3, 224, 224) +output_shape = (batch_size, num_of_image_class) +relay_mod, relay_params = resnet.get_workload(num_layers=18, batch_size=1, image_shape=image_shape) +print("Printing the IR module...") +print(relay_mod.astext(show_meta_data=False)) +``` + +输出结果: + +``` bash +Printing the IR module... +#[version = "0.0.5"] +def @main(%data: Tensor[(1, 3, 224, 224), float32] /* ty=Tensor[(1, 3, 224, 224), float32] */, %bn_data_gamma: Tensor[(3), float32] /* ty=Tensor[(3), float32] */, %bn_data_beta: Tensor[(3), float32] /* ty=Tensor[(3), float32] */, %bn_data_moving_mean: Tensor[(3), float32] /* ty=Tensor[(3), float32] */, %bn_data_moving_var: Tensor[(3), float32] /* ty=Tensor[(3), float32] */, %conv0_weight: Tensor[(64, 3, 7, 7), float32] /* ty=Tensor[(64, 3, 7, 7), float32] */, %bn0_gamma: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %bn0_beta: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %bn0_moving_mean: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %bn0_moving_var: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit1_bn1_gamma: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit1_bn1_beta: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit1_bn1_moving_mean: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit1_bn1_moving_var: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit1_conv1_weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */, %stage1_unit1_bn2_gamma: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit1_bn2_beta: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit1_bn2_moving_mean: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit1_bn2_moving_var: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit1_conv2_weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */, %stage1_unit1_sc_weight: Tensor[(64, 64, 1, 1), float32] /* ty=Tensor[(64, 64, 1, 1), float32] */, %stage1_unit2_bn1_gamma: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit2_bn1_beta: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit2_bn1_moving_mean: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit2_bn1_moving_var: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit2_conv1_weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */, %stage1_unit2_bn2_gamma: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit2_bn2_beta: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit2_bn2_moving_mean: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit2_bn2_moving_var: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit2_conv2_weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */, %stage2_unit1_bn1_gamma: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage2_unit1_bn1_beta: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage2_unit1_bn1_moving_mean: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage2_unit1_bn1_moving_var: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage2_unit1_conv1_weight: Tensor[(128, 64, 3, 3), float32] /* ty=Tensor[(128, 64, 3, 3), float32] */, %stage2_unit1_bn2_gamma: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit1_bn2_beta: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit1_bn2_moving_mean: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit1_bn2_moving_var: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit1_conv2_weight: Tensor[(128, 128, 3, 3), float32] /* ty=Tensor[(128, 128, 3, 3), float32] */, %stage2_unit1_sc_weight: Tensor[(128, 64, 1, 1), float32] /* ty=Tensor[(128, 64, 1, 1), float32] */, %stage2_unit2_bn1_gamma: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit2_bn1_beta: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit2_bn1_moving_mean: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit2_bn1_moving_var: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit2_conv1_weight: Tensor[(128, 128, 3, 3), float32] /* ty=Tensor[(128, 128, 3, 3), float32] */, %stage2_unit2_bn2_gamma: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit2_bn2_beta: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit2_bn2_moving_mean: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit2_bn2_moving_var: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit2_conv2_weight: Tensor[(128, 128, 3, 3), float32] /* ty=Tensor[(128, 128, 3, 3), float32] */, %stage3_unit1_bn1_gamma: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage3_unit1_bn1_beta: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage3_unit1_bn1_moving_mean: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage3_unit1_bn1_moving_var: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage3_unit1_conv1_weight: Tensor[(256, 128, 3, 3), float32] /* ty=Tensor[(256, 128, 3, 3), float32] */, %stage3_unit1_bn2_gamma: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit1_bn2_beta: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit1_bn2_moving_mean: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit1_bn2_moving_var: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit1_conv2_weight: Tensor[(256, 256, 3, 3), float32] /* ty=Tensor[(256, 256, 3, 3), float32] */, %stage3_unit1_sc_weight: Tensor[(256, 128, 1, 1), float32] /* ty=Tensor[(256, 128, 1, 1), float32] */, %stage3_unit2_bn1_gamma: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit2_bn1_beta: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit2_bn1_moving_mean: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit2_bn1_moving_var: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit2_conv1_weight: Tensor[(256, 256, 3, 3), float32] /* ty=Tensor[(256, 256, 3, 3), float32] */, %stage3_unit2_bn2_gamma: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit2_bn2_beta: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit2_bn2_moving_mean: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit2_bn2_moving_var: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit2_conv2_weight: Tensor[(256, 256, 3, 3), float32] /* ty=Tensor[(256, 256, 3, 3), float32] */, %stage4_unit1_bn1_gamma: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage4_unit1_bn1_beta: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage4_unit1_bn1_moving_mean: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage4_unit1_bn1_moving_var: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage4_unit1_conv1_weight: Tensor[(512, 256, 3, 3), float32] /* ty=Tensor[(512, 256, 3, 3), float32] */, %stage4_unit1_bn2_gamma: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit1_bn2_beta: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit1_bn2_moving_mean: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit1_bn2_moving_var: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit1_conv2_weight: Tensor[(512, 512, 3, 3), float32] /* ty=Tensor[(512, 512, 3, 3), float32] */, %stage4_unit1_sc_weight: Tensor[(512, 256, 1, 1), float32] /* ty=Tensor[(512, 256, 1, 1), float32] */, %stage4_unit2_bn1_gamma: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit2_bn1_beta: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit2_bn1_moving_mean: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit2_bn1_moving_var: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit2_conv1_weight: Tensor[(512, 512, 3, 3), float32] /* ty=Tensor[(512, 512, 3, 3), float32] */, %stage4_unit2_bn2_gamma: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit2_bn2_beta: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit2_bn2_moving_mean: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit2_bn2_moving_var: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit2_conv2_weight: Tensor[(512, 512, 3, 3), float32] /* ty=Tensor[(512, 512, 3, 3), float32] */, %bn1_gamma: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %bn1_beta: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %bn1_moving_mean: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %bn1_moving_var: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %fc1_weight: Tensor[(1000, 512), float32] /* ty=Tensor[(1000, 512), float32] */, %fc1_bias: Tensor[(1000), float32] /* ty=Tensor[(1000), float32] */) -> Tensor[(1, 1000), float32] { + %0 = nn.batch_norm(%data, %bn_data_gamma, %bn_data_beta, %bn_data_moving_mean, %bn_data_moving_var, epsilon=2e-05f, scale=False) /* ty=(Tensor[(1, 3, 224, 224), float32], Tensor[(3), float32], Tensor[(3), float32]) */; + %1 = %0.0 /* ty=Tensor[(1, 3, 224, 224), float32] */; + %2 = nn.conv2d(%1, %conv0_weight, strides=[2, 2], padding=[3, 3, 3, 3], channels=64, kernel_size=[7, 7]) /* ty=Tensor[(1, 64, 112, 112), float32] */; + %3 = nn.batch_norm(%2, %bn0_gamma, %bn0_beta, %bn0_moving_mean, %bn0_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 64, 112, 112), float32], Tensor[(64), float32], Tensor[(64), float32]) */; + %4 = %3.0 /* ty=Tensor[(1, 64, 112, 112), float32] */; + %5 = nn.relu(%4) /* ty=Tensor[(1, 64, 112, 112), float32] */; + %6 = nn.max_pool2d(%5, pool_size=[3, 3], strides=[2, 2], padding=[1, 1, 1, 1]) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %7 = nn.batch_norm(%6, %stage1_unit1_bn1_gamma, %stage1_unit1_bn1_beta, %stage1_unit1_bn1_moving_mean, %stage1_unit1_bn1_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 64, 56, 56), float32], Tensor[(64), float32], Tensor[(64), float32]) */; + %8 = %7.0 /* ty=Tensor[(1, 64, 56, 56), float32] */; + %9 = nn.relu(%8) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %10 = nn.conv2d(%9, %stage1_unit1_conv1_weight, padding=[1, 1, 1, 1], channels=64, kernel_size=[3, 3]) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %11 = nn.batch_norm(%10, %stage1_unit1_bn2_gamma, %stage1_unit1_bn2_beta, %stage1_unit1_bn2_moving_mean, %stage1_unit1_bn2_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 64, 56, 56), float32], Tensor[(64), float32], Tensor[(64), float32]) */; + %12 = %11.0 /* ty=Tensor[(1, 64, 56, 56), float32] */; + %13 = nn.relu(%12) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %14 = nn.conv2d(%13, %stage1_unit1_conv2_weight, padding=[1, 1, 1, 1], channels=64, kernel_size=[3, 3]) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %15 = nn.conv2d(%9, %stage1_unit1_sc_weight, padding=[0, 0, 0, 0], channels=64, kernel_size=[1, 1]) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %16 = add(%14, %15) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %17 = nn.batch_norm(%16, %stage1_unit2_bn1_gamma, %stage1_unit2_bn1_beta, %stage1_unit2_bn1_moving_mean, %stage1_unit2_bn1_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 64, 56, 56), float32], Tensor[(64), float32], Tensor[(64), float32]) */; + %18 = %17.0 /* ty=Tensor[(1, 64, 56, 56), float32] */; + %19 = nn.relu(%18) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %20 = nn.conv2d(%19, %stage1_unit2_conv1_weight, padding=[1, 1, 1, 1], channels=64, kernel_size=[3, 3]) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %21 = nn.batch_norm(%20, %stage1_unit2_bn2_gamma, %stage1_unit2_bn2_beta, %stage1_unit2_bn2_moving_mean, %stage1_unit2_bn2_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 64, 56, 56), float32], Tensor[(64), float32], Tensor[(64), float32]) */; + %22 = %21.0 /* ty=Tensor[(1, 64, 56, 56), float32] */; + %23 = nn.relu(%22) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %24 = nn.conv2d(%23, %stage1_unit2_conv2_weight, padding=[1, 1, 1, 1], channels=64, kernel_size=[3, 3]) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %25 = add(%24, %16) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %26 = nn.batch_norm(%25, %stage2_unit1_bn1_gamma, %stage2_unit1_bn1_beta, %stage2_unit1_bn1_moving_mean, %stage2_unit1_bn1_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 64, 56, 56), float32], Tensor[(64), float32], Tensor[(64), float32]) */; + %27 = %26.0 /* ty=Tensor[(1, 64, 56, 56), float32] */; + %28 = nn.relu(%27) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %29 = nn.conv2d(%28, %stage2_unit1_conv1_weight, strides=[2, 2], padding=[1, 1, 1, 1], channels=128, kernel_size=[3, 3]) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %30 = nn.batch_norm(%29, %stage2_unit1_bn2_gamma, %stage2_unit1_bn2_beta, %stage2_unit1_bn2_moving_mean, %stage2_unit1_bn2_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 128, 28, 28), float32], Tensor[(128), float32], Tensor[(128), float32]) */; + %31 = %30.0 /* ty=Tensor[(1, 128, 28, 28), float32] */; + %32 = nn.relu(%31) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %33 = nn.conv2d(%32, %stage2_unit1_conv2_weight, padding=[1, 1, 1, 1], channels=128, kernel_size=[3, 3]) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %34 = nn.conv2d(%28, %stage2_unit1_sc_weight, strides=[2, 2], padding=[0, 0, 0, 0], channels=128, kernel_size=[1, 1]) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %35 = add(%33, %34) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %36 = nn.batch_norm(%35, %stage2_unit2_bn1_gamma, %stage2_unit2_bn1_beta, %stage2_unit2_bn1_moving_mean, %stage2_unit2_bn1_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 128, 28, 28), float32], Tensor[(128), float32], Tensor[(128), float32]) */; + %37 = %36.0 /* ty=Tensor[(1, 128, 28, 28), float32] */; + %38 = nn.relu(%37) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %39 = nn.conv2d(%38, %stage2_unit2_conv1_weight, padding=[1, 1, 1, 1], channels=128, kernel_size=[3, 3]) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %40 = nn.batch_norm(%39, %stage2_unit2_bn2_gamma, %stage2_unit2_bn2_beta, %stage2_unit2_bn2_moving_mean, %stage2_unit2_bn2_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 128, 28, 28), float32], Tensor[(128), float32], Tensor[(128), float32]) */; + %41 = %40.0 /* ty=Tensor[(1, 128, 28, 28), float32] */; + %42 = nn.relu(%41) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %43 = nn.conv2d(%42, %stage2_unit2_conv2_weight, padding=[1, 1, 1, 1], channels=128, kernel_size=[3, 3]) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %44 = add(%43, %35) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %45 = nn.batch_norm(%44, %stage3_unit1_bn1_gamma, %stage3_unit1_bn1_beta, %stage3_unit1_bn1_moving_mean, %stage3_unit1_bn1_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 128, 28, 28), float32], Tensor[(128), float32], Tensor[(128), float32]) */; + %46 = %45.0 /* ty=Tensor[(1, 128, 28, 28), float32] */; + %47 = nn.relu(%46) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %48 = nn.conv2d(%47, %stage3_unit1_conv1_weight, strides=[2, 2], padding=[1, 1, 1, 1], channels=256, kernel_size=[3, 3]) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %49 = nn.batch_norm(%48, %stage3_unit1_bn2_gamma, %stage3_unit1_bn2_beta, %stage3_unit1_bn2_moving_mean, %stage3_unit1_bn2_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 256, 14, 14), float32], Tensor[(256), float32], Tensor[(256), float32]) */; + %50 = %49.0 /* ty=Tensor[(1, 256, 14, 14), float32] */; + %51 = nn.relu(%50) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %52 = nn.conv2d(%51, %stage3_unit1_conv2_weight, padding=[1, 1, 1, 1], channels=256, kernel_size=[3, 3]) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %53 = nn.conv2d(%47, %stage3_unit1_sc_weight, strides=[2, 2], padding=[0, 0, 0, 0], channels=256, kernel_size=[1, 1]) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %54 = add(%52, %53) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %55 = nn.batch_norm(%54, %stage3_unit2_bn1_gamma, %stage3_unit2_bn1_beta, %stage3_unit2_bn1_moving_mean, %stage3_unit2_bn1_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 256, 14, 14), float32], Tensor[(256), float32], Tensor[(256), float32]) */; + %56 = %55.0 /* ty=Tensor[(1, 256, 14, 14), float32] */; + %57 = nn.relu(%56) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %58 = nn.conv2d(%57, %stage3_unit2_conv1_weight, padding=[1, 1, 1, 1], channels=256, kernel_size=[3, 3]) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %59 = nn.batch_norm(%58, %stage3_unit2_bn2_gamma, %stage3_unit2_bn2_beta, %stage3_unit2_bn2_moving_mean, %stage3_unit2_bn2_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 256, 14, 14), float32], Tensor[(256), float32], Tensor[(256), float32]) */; + %60 = %59.0 /* ty=Tensor[(1, 256, 14, 14), float32] */; + %61 = nn.relu(%60) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %62 = nn.conv2d(%61, %stage3_unit2_conv2_weight, padding=[1, 1, 1, 1], channels=256, kernel_size=[3, 3]) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %63 = add(%62, %54) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %64 = nn.batch_norm(%63, %stage4_unit1_bn1_gamma, %stage4_unit1_bn1_beta, %stage4_unit1_bn1_moving_mean, %stage4_unit1_bn1_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 256, 14, 14), float32], Tensor[(256), float32], Tensor[(256), float32]) */; + %65 = %64.0 /* ty=Tensor[(1, 256, 14, 14), float32] */; + %66 = nn.relu(%65) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %67 = nn.conv2d(%66, %stage4_unit1_conv1_weight, strides=[2, 2], padding=[1, 1, 1, 1], channels=512, kernel_size=[3, 3]) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %68 = nn.batch_norm(%67, %stage4_unit1_bn2_gamma, %stage4_unit1_bn2_beta, %stage4_unit1_bn2_moving_mean, %stage4_unit1_bn2_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 512, 7, 7), float32], Tensor[(512), float32], Tensor[(512), float32]) */; + %69 = %68.0 /* ty=Tensor[(1, 512, 7, 7), float32] */; + %70 = nn.relu(%69) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %71 = nn.conv2d(%70, %stage4_unit1_conv2_weight, padding=[1, 1, 1, 1], channels=512, kernel_size=[3, 3]) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %72 = nn.conv2d(%66, %stage4_unit1_sc_weight, strides=[2, 2], padding=[0, 0, 0, 0], channels=512, kernel_size=[1, 1]) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %73 = add(%71, %72) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %74 = nn.batch_norm(%73, %stage4_unit2_bn1_gamma, %stage4_unit2_bn1_beta, %stage4_unit2_bn1_moving_mean, %stage4_unit2_bn1_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 512, 7, 7), float32], Tensor[(512), float32], Tensor[(512), float32]) */; + %75 = %74.0 /* ty=Tensor[(1, 512, 7, 7), float32] */; + %76 = nn.relu(%75) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %77 = nn.conv2d(%76, %stage4_unit2_conv1_weight, padding=[1, 1, 1, 1], channels=512, kernel_size=[3, 3]) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %78 = nn.batch_norm(%77, %stage4_unit2_bn2_gamma, %stage4_unit2_bn2_beta, %stage4_unit2_bn2_moving_mean, %stage4_unit2_bn2_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 512, 7, 7), float32], Tensor[(512), float32], Tensor[(512), float32]) */; + %79 = %78.0 /* ty=Tensor[(1, 512, 7, 7), float32] */; + %80 = nn.relu(%79) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %81 = nn.conv2d(%80, %stage4_unit2_conv2_weight, padding=[1, 1, 1, 1], channels=512, kernel_size=[3, 3]) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %82 = add(%81, %73) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %83 = nn.batch_norm(%82, %bn1_gamma, %bn1_beta, %bn1_moving_mean, %bn1_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 512, 7, 7), float32], Tensor[(512), float32], Tensor[(512), float32]) */; + %84 = %83.0 /* ty=Tensor[(1, 512, 7, 7), float32] */; + %85 = nn.relu(%84) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %86 = nn.global_avg_pool2d(%85) /* ty=Tensor[(1, 512, 1, 1), float32] */; + %87 = nn.batch_flatten(%86) /* ty=Tensor[(1, 512), float32] */; + %88 = nn.dense(%87, %fc1_weight, units=1000) /* ty=Tensor[(1, 1000), float32] */; + %89 = nn.bias_add(%88, %fc1_bias, axis=-1) /* ty=Tensor[(1, 1000), float32] */; + nn.softmax(%89) /* ty=Tensor[(1, 1000), float32] */ +} +``` + +## 使用 Instrument 创建 PassContext + +要用 instrument 运行所有 Pass,将其通过参数 `instruments` 传递给构造函数 `PassContext`。`PassTimingInstrument` 用于分析每个 Pass 执行时间的内置函数。 + +``` python +timing_inst = PassTimingInstrument() +with tvm.transform.PassContext(instruments=[timing_inst]): + relay_mod = relay.transform.InferType()(relay_mod) + relay_mod = relay.transform.FoldScaleAxis()(relay_mod) + # 在退出上下文之前,获取配置文件结果。 + profiles = timing_inst.render() +print("Printing results of timing profile...") +print(profiles) +``` + +输出结果: + +``` bash +Printing results of timing profile... +InferType: 6628us [6628us] (46.29%; 46.29%) +FoldScaleAxis: 7691us [6us] (53.71%; 53.71%) + FoldConstant: 7685us [1578us] (53.67%; 99.92%) + InferType: 6107us [6107us] (42.65%; 79.47%) +``` + +## 将当前 PassContext 与 Instrument 一起使用 + +也可以使用当前的 `PassContext`,并通过 `override_instruments` 方法注册 `PassInstrument` 实例。注意,如果已经存在了任何 instrument,`override_instruments` 将执行 `exit_pass_ctx` 方法。然后它切换到新 instrument,并调用新 instrument 的 `enter_pass_ctx` 方法。有关这些方法,参阅以下部分和 `tvm.instrument.pass_instrument()`。 + +``` python +cur_pass_ctx = tvm.transform.PassContext.current() +cur_pass_ctx.override_instruments([timing_inst]) +relay_mod = relay.transform.InferType()(relay_mod) +relay_mod = relay.transform.FoldScaleAxis()(relay_mod) +profiles = timing_inst.render() +print("Printing results of timing profile...") +print(profiles) +``` + +输出结果: + +``` bash +Printing results of timing profile... +InferType: 6131us [6131us] (44.86%; 44.86%) +FoldScaleAxis: 7536us [4us] (55.14%; 55.14%) + FoldConstant: 7532us [1549us] (55.11%; 99.94%) + InferType: 5982us [5982us] (43.77%; 79.43%) +``` + +注册空列表以清除现有 instrument。 + +注意,`PassTimingInstrument` 的 `exit_pass_ctx` 被调用了。配置文件被清除,因此不会打印任何内容。 + +``` python +cur_pass_ctx.override_instruments([]) +# 取消 .render() 的注释以查看如下警告: +# 警告:没有 Pass 分析,您是否启用了 Pass 分析? +# profiles = timing_inst.render() +``` + +## 创建自定义 Instrument 类 + +可以使用 `tvm.instrument.pass_instrument()` 装饰器创建自定义 instrument 类。 + +创建一个工具类(计算每次 Pass 引起的每个算子出现次数的变化)。可以在 Pass 之前和之后查看 `op.name` 来找到每个算子的名称,从而计算差异。 + +``` python +@pass_instrument +class RelayCallNodeDiffer: + def __init__(self): + self._op_diff = [] + # Pass 可以嵌套。 + # 使用堆栈来确保得到之前/之后正确的 pairs。 + self._op_cnt_before_stack = [] + + def enter_pass_ctx(self): + self._op_diff = [] + self._op_cnt_before_stack = [] + + def exit_pass_ctx(self): + assert len(self._op_cnt_before_stack) == 0, "The stack is not empty. Something wrong." + + def run_before_pass(self, mod, info): + self._op_cnt_before_stack.append((info.name, self._count_nodes(mod))) + + def run_after_pass(self, mod, info): + # 弹出最新记录的 Pass。 + name_before, op_to_cnt_before = self._op_cnt_before_stack.pop() + assert name_before == info.name, "name_before: {}, info.name: {} doesn't match".format( + name_before, info.name + ) + cur_depth = len(self._op_cnt_before_stack) + op_to_cnt_after = self._count_nodes(mod) + op_diff = self._diff(op_to_cnt_after, op_to_cnt_before) + # 只记导致差异的 Pass。 + if op_diff: + self._op_diff.append((cur_depth, info.name, op_diff)) + + def get_pass_to_op_diff(self): + """ + return [ + (depth, pass_name, {op_name: diff_num, ...}), ... + ] + """ + return self._op_diff + + @staticmethod + def _count_nodes(mod): + """Count the number of occurrences of each operator in the module""" + ret = {} + + def visit(node): + if isinstance(node, relay.expr.Call): + if hasattr(node.op, "name"): + op_name = node.op.name + else: + # 某些 CallNode 可能没有“名称”,例如 relay.Function + return + ret[op_name] = ret.get(op_name, 0) + 1 + + relay.analysis.post_order_visit(mod["main"], visit) + return ret + + @staticmethod + def _diff(d_after, d_before): + """Calculate the difference of two dictionary along their keys. + The result is values in d_after minus values in d_before. + """ + ret = {} + key_after, key_before = set(d_after), set(d_before) + for k in key_before & key_after: + tmp = d_after[k] - d_before[k] + if tmp: + ret[k] = d_after[k] - d_before[k] + for k in key_after - key_before: + ret[k] = d_after[k] + for k in key_before - key_after: + ret[k] = -d_before[k] + return ret +``` + +## 应用 Pass 和多个 Instrument 类 + +可以在 `PassContext` 中使用多个 instrument 类。但注意,instrument 方法是按 `instruments` 参数的顺序执行的,所以对于像 `PassTimingInstrument` 这样的 instrument 类,不可避免地要将其他 instrument 类的执行时间计入最终的分析结果。 + +``` python +call_node_inst = RelayCallNodeDiffer() +desired_layouts = { + "nn.conv2d": ["NHWC", "HWIO"], +} +pass_seq = tvm.transform.Sequential( + [ + relay.transform.FoldConstant(), + relay.transform.ConvertLayout(desired_layouts), + relay.transform.FoldConstant(), + ] +) +relay_mod["main"] = bind_params_by_name(relay_mod["main"], relay_params) +# timing_inst 放在 call_node_inst 之后。 +# 所以 `call_node.inst.run_after_pass()` 的执行时间也算在内。 +with tvm.transform.PassContext(opt_level=3, instruments=[call_node_inst, timing_inst]): + relay_mod = pass_seq(relay_mod) + profiles = timing_inst.render() +# 取消注释下一行以查看时序配置文件结果。 +# print(profiles) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +可以看到每个操作类型增加/减少了多少 CallNode。 + +``` python +from pprint import pprint + +print("Printing the change in number of occurrences of each operator caused by each pass...") +pprint(call_node_inst.get_pass_to_op_diff()) +``` + +输出结果: + +``` bash +Printing the change in number of occurrences of each operator caused by each pass... +[(1, 'CanonicalizeOps', {'add': 1, 'nn.bias_add': -1}), + (1, 'ConvertLayout', {'expand_dims': 1, 'layout_transform': 23}), + (1, 'FoldConstant', {'expand_dims': -1, 'layout_transform': -21}), + (0, 'sequential', {'add': 1, 'layout_transform': 2, 'nn.bias_add': -1})] +``` + +## 异常处理 + +以下演示了 `PassInstrument` 的方法发生异常的详细情况。 + +定义在进入/退出 `PassContext` 中引发异常的 `PassInstrument` 类: + +```plain +class PassExampleBase: + def __init__(self, name): + self._name = name + + def enter_pass_ctx(self): + print(self._name, "enter_pass_ctx") + + def exit_pass_ctx(self): + print(self._name, "exit_pass_ctx") + + def should_run(self, mod, info): + print(self._name, "should_run") + return True + + def run_before_pass(self, mod, pass_info): + print(self._name, "run_before_pass") + + def run_after_pass(self, mod, pass_info): + print(self._name, "run_after_pass") + +@pass_instrument +class PassFine(PassExampleBase): + pass + +@pass_instrument +class PassBadEnterCtx(PassExampleBase): + def enter_pass_ctx(self): + print(self._name, "bad enter_pass_ctx!!!") + raise ValueError("{} bad enter_pass_ctx".format(self._name)) + +@pass_instrument +class PassBadExitCtx(PassExampleBase): + def exit_pass_ctx(self): + print(self._name, "bad exit_pass_ctx!!!") + raise ValueError("{} bad exit_pass_ctx".format(self._name)) +``` + +若 `enter_pass_ctx` 发生异常,`PassContext` 将禁用 pass instrumentation。它将运行每个成功完成 `enter_pass_ctx` 的 PassInstrument 的 `exit_pass_ctx`。 + +下面的例子可以看到 *PassFine_0* 的 `exit_pass_ctx` 在异常后执行。 + +``` python +demo_ctx = tvm.transform.PassContext( + instruments=[ + PassFine("PassFine_0"), + PassBadEnterCtx("PassBadEnterCtx"), + PassFine("PassFine_1"), + ] +) +try: + with demo_ctx: + relay_mod = relay.transform.InferType()(relay_mod) +except ValueError as ex: + print("Catching", str(ex).split("\n")[-1]) +``` + +输出结果: + +``` bash +PassFine_0 enter_pass_ctx +PassBadEnterCtx bad enter_pass_ctx!!! +PassFine_0 exit_pass_ctx +Catching ValueError: PassBadEnterCtx bad enter_pass_ctx +``` + +`PassInstrument` 实例中的异常会导致当前的 `PassContext` 所有 instrument 被清除,因此调用 `override_instruments` 时不会打印任何内容。 + +``` python +demo_ctx.override_instruments([]) # 没有打印 PassFine_0 exit_pass_ctx....等 +``` + +若 `exit_pass_ctx` 发生异常,则禁用 pass instrument,然后传播异常。这意味着 `PassInstrument` 在抛出异常之后注册的实例不会执行 `exit_pass_ctx`。 + +``` python +demo_ctx = tvm.transform.PassContext( + instruments=[ + PassFine("PassFine_0"), + PassBadExitCtx("PassBadExitCtx"), + PassFine("PassFine_1"), + ] +) +try: + # PassFine_1 执行 enter_pass_ctx,但不执行 exit_pass_ctx。 + with demo_ctx: + relay_mod = relay.transform.InferType()(relay_mod) +except ValueError as ex: + print("Catching", str(ex).split("\n")[-1]) +``` + +输出结果: + +``` bash +PassFine_0 enter_pass_ctx +PassBadExitCtx enter_pass_ctx +PassFine_1 enter_pass_ctx +PassFine_0 should_run +PassBadExitCtx should_run +PassFine_1 should_run +PassFine_0 run_before_pass +PassBadExitCtx run_before_pass +PassFine_1 run_before_pass +PassFine_0 run_after_pass +PassBadExitCtx run_after_pass +PassFine_1 run_after_pass +PassFine_0 exit_pass_ctx +PassBadExitCtx bad exit_pass_ctx!!! +Catching ValueError: PassBadExitCtx bad exit_pass_ctx +``` + +以 `run_before_pass`为例: + +`should_run`、`run_before_pass` 和 `run_after_pass` 发生的异常没有明确处理,用上下文管理器(`with` 语法)安全退出 `PassContext`。 + +``` python +@pass_instrument +class PassBadRunBefore(PassExampleBase): + def run_before_pass(self, mod, pass_info): + print(self._name, "bad run_before_pass!!!") + raise ValueError("{} bad run_before_pass".format(self._name)) + +demo_ctx = tvm.transform.PassContext( + instruments=[ + PassFine("PassFine_0"), + PassBadRunBefore("PassBadRunBefore"), + PassFine("PassFine_1"), + ] +) +try: + # 所有的 exit_pass_ctx 都会被调用。 + with demo_ctx: + relay_mod = relay.transform.InferType()(relay_mod) +except ValueError as ex: + print("Catching", str(ex).split("\n")[-1]) +``` + +输出结果: + +``` bash +PassFine_0 enter_pass_ctx +PassBadRunBefore enter_pass_ctx +PassFine_1 enter_pass_ctx +PassFine_0 should_run +PassBadRunBefore should_run +PassFine_1 should_run +PassFine_0 run_before_pass +PassBadRunBefore bad run_before_pass!!! +PassFine_0 exit_pass_ctx +PassBadRunBefore exit_pass_ctx +PassFine_1 exit_pass_ctx +Catching ValueError: PassBadRunBefore bad run_before_pass +``` + +注意,pass instrumentation 未禁用。所以若调用 `override_instruments`,`exit_pass_ctx` 先前注册的 `PassInstrument` 将被调用。 + +``` python +demo_ctx.override_instruments([]) +``` + +输出结果: + +``` bash +PassFine_0 exit_pass_ctx +PassBadRunBefore exit_pass_ctx +PassFine_1 exit_pass_ctx +``` + +若不用 `with` 语法包装 pass 执行,则不会调用 `exit_pass_ctx`。用当前的 `PassContext`: + +``` python +cur_pass_ctx = tvm.transform.PassContext.current() +cur_pass_ctx.override_instruments( + [ + PassFine("PassFine_0"), + PassBadRunBefore("PassBadRunBefore"), + PassFine("PassFine_1"), + ] +) +``` + +输出结果: + +``` bash +PassFine_0 enter_pass_ctx +PassBadRunBefore enter_pass_ctx +PassFine_1 enter_pass_ctx +``` + +然后调用 Pass。异常后 `exit_pass_ctx` 不执行。 + +``` python +try: + # No ``exit_pass_ctx`` got executed. + relay_mod = relay.transform.InferType()(relay_mod) +except ValueError as ex: + print("Catching", str(ex).split("\n")[-1]) +``` + +输出结果: + +``` bash +PassFine_0 should_run +PassBadRunBefore should_run +PassFine_1 should_run +PassFine_0 run_before_pass +PassBadRunBefore bad run_before_pass!!! +Catching ValueError: PassBadRunBefore bad run_before_pass +``` + +清除 instrument。 + +``` python +cur_pass_ctx.override_instruments([]) +``` + +输出结果: + +``` bash +PassFine_0 exit_pass_ctx +PassBadRunBefore exit_pass_ctx +PassFine_1 exit_pass_ctx +``` + +[下载 Python 源代码:use_pass_instrument.py](https://tvm.apache.org/docs/_downloads/d0a1817b910da958b41d88afe4d4952d/use_pass_instrument.py) + +[下载 Jupyter Notebook:use_pass_instrument.ipynb](https://tvm.apache.org/docs/_downloads/f6ff0fbc61d45d2cc0f53ebbf11a5fb5/use_pass_instrument.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/extend/04-datatypes.md b/versioned_docs/version-0.12.0/how_to/extend/04-datatypes.md new file mode 100644 index 00000000..209cbd41 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/extend/04-datatypes.md @@ -0,0 +1,468 @@ +--- +title: 在 TVM 中使用 Bring Your Own Datatypes +--- + +# 在 TVM 中使用 Bring Your Own Datatypes + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/extend_tvm/bring_your_own_datatypes.html#sphx-glr-download-how-to-extend-tvm-bring-your-own-datatypes-py) 下载完整的示例代码 +::: + +**作者**:[Gus Smith](https://github.com/gussmith23), [Andrew Liu](https://github.com/hypercubestart) + +本教程将展示如何利用 Bring Your Own Datatypes 框架在 TVM 中使用自定义数据类型。注意,Bring Your Own Datatypes 框架目前仅处理**数据类型的软件模拟版本**。该框架不支持开箱即用地编译自定义加速器数据类型。 + +## 数据类型库 + +Bring Your Own Datatypes 允许用户在 TVM 的原生数据类型(例如 `float`)旁边注册自己的数据类型实现。这些数据类型实现通常以库的形式出现。例如: + +* [libposit](https://github.com/cjdelisle/libposit),一个位置库 +* [Stillwater Universal](https://github.com/stillwater-sc/universal),一个包含位置、定点数和其他类型的库 +* [SoftFloat](https://github.com/ucb-bar/berkeley-softfloat-3),伯克利的 IEEE 754 浮点软件实现 + +Bring Your Own Datatypes 使用户能够将这些数据类型实现插入 TVM! + +本节中我们将用到一个已经实现的示例库(位于 `3rdparty/byodt/myfloat.cc`)。这种称之为「myfloat」的数据类型实际上只是一个 IEE-754 浮点数,但它提供了一个有用的示例,表明任何数据类型都可以在 BYODT 框架中使用。 + +## 设置 + +由于不使用任何 3rdparty 库,因此无需设置。 + +若要用自己的数据类型库尝试,首先用 `CDLL` 把库的函数引入进程空间: + +``` cmake +ctypes.CDLL('my-datatype-lib.so', ctypes.RTLD_GLOBAL) +``` + +## 一个简单的 TVM 程序 + +从在 TVM 中编写一个简单的程序开始,之后进行重写,从而使用自定义数据类型。 + +``` python +import tvm +from tvm import relay + +# 基本程序:Z = X + Y +x = relay.var("x", shape=(3,), dtype="float32") +y = relay.var("y", shape=(3,), dtype="float32") +z = x + y +program = relay.Function([x, y], z) +module = tvm.IRModule.from_expr(program) +``` + +现使用 numpy 为程序创建随机输入: + +``` python +import numpy as np + +np.random.seed(23) # 可重复性 + +x_input = np.random.rand(3).astype("float32") +y_input = np.random.rand(3).astype("float32") +print("x: {}".format(x_input)) +print("y: {}".format(y_input)) +``` + +输出结果: + +``` bash +x: [0.51729786 0.9469626 0.7654598 ] +y: [0.28239584 0.22104536 0.6862221 ] +``` + +最后,准备运行程序: + +``` python +z_output = relay.create_executor(mod=module).evaluate()(x_input, y_input) +print("z: {}".format(z_output)) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +z: [0.7996937 1.168008 1.4516819] +``` + +## 添加自定义数据类型 + +接下来使用自定义数据类型进行中间计算。 + +使用与上面相同的输入变量 `x` 和 `y`,但在添加 `x + y` 之前,首先通过调用 `relay.cast(...)` 将 `x` 和 `y` 转换为自定义数据类型。 + +注意如何指定自定义数据类型:使用特殊的 `custom[...]` 语法来表示。此外,注意数据类型后面的「32」:这是自定义数据类型的位宽,告诉 TVM `myfloat` 的每个实例都是 32 位宽。 + +``` python +try: + with tvm.transform.PassContext(config={"tir.disable_vectorize": True}): + x_myfloat = relay.cast(x, dtype="custom[myfloat]32") + y_myfloat = relay.cast(y, dtype="custom[myfloat]32") + z_myfloat = x_myfloat + y_myfloat + z = relay.cast(z_myfloat, dtype="float32") +except tvm.TVMError as e: + # 打印最后一行错误 + print(str(e).split("\n")[-1]) +``` + +尝试生成此程序会从 TVM 引发错误。 TVM 不知道如何创造性地处理所有自定义数据类型!因此首先要向 TVM 注册自定义类型,给它一个名称和一个类型代码: + +``` python +tvm.target.datatype.register("myfloat", 150) +``` + +注意,类型代码 150 目前由用户手动选择。参阅 [include/tvm/runtime/c_runtime_api.h](https://github.com/apache/tvm/blob/main/include/tvm/runtime/data_type.h) 中的 `TVMTypeCode::kCustomBegin`。下面再次生成程序: + +``` python +x_myfloat = relay.cast(x, dtype="custom[myfloat]32") +y_myfloat = relay.cast(y, dtype="custom[myfloat]32") +z_myfloat = x_myfloat + y_myfloat +z = relay.cast(z_myfloat, dtype="float32") +program = relay.Function([x, y], z) +module = tvm.IRModule.from_expr(program) +module = relay.transform.InferType()(module) +``` + +现在有了一个使用 myfloat 的 Relay 程序! + +``` python +print(program) +``` + +输出结果: + +``` bash +fn (%x: Tensor[(3), float32], %y: Tensor[(3), float32]) { + %0 = cast(%x, dtype="custom[myfloat]32"); + %1 = cast(%y, dtype="custom[myfloat]32"); + %2 = add(%0, %1); + cast(%2, dtype="float32") +} +``` + +现在可以准确无误地表达程序,尝试运行! + +``` python +try: + with tvm.transform.PassContext(config={"tir.disable_vectorize": True}): + z_output_myfloat = relay.create_executor("graph", mod=module).evaluate()(x_input, y_input) + print("z: {}".format(y_myfloat)) +except tvm.TVMError as e: + # 打印最后一行错误 + print(str(e).split("\n")[-1]) +``` + +输出结果: + +``` bash +Check failed: (lower) is false: Cast lowering function for target llvm destination type 150 source type 2 not found +``` + +编译该程序会引发错误,下面来剖析这个报错。 + +该报错发生在代码降级的过程中,即将自定义数据类型代码,降级为 TVM 可以编译和运行的代码。TVM 显示,当从源类型 2(`float`,在 TVM 中)转换到目标类型 150(自定义数据类型)时,它无法找到 `Cast` 操作的*降级函数*。 + +当对自定义数据类型进行降级时,若 TVM 遇到对自定义数据类型的操作,它会查找用户注册的*降级函数*,这个函数告诉 TVM 如何将操作降级为 TVM 理解的数据类型的操作。由于我们还没有告诉 TVM 如何降级自定义数据类型的 `Cast` 操作,因此会报错。 + +要修复这个错误,只需要指定一个降级函数: + +``` python +tvm.target.datatype.register_op( + tvm.target.datatype.create_lower_func( + { + (32, 32): "FloatToCustom32", # cast from float32 to myfloat32 # 从 float32 转换为 myfloat32 + } + ), + "Cast", + "llvm", + "float", + "myfloat", +) +``` + +`register_op(...)` 调用接受一个降级函数和一些参数,这些参数准确地指定了应该使用提供的降级函数降级的操作。在这种情况下,传递的参数指定此降级函数用于将 target `“llvm”` 的 `Cast` 从 `float` 降级到 `myfloat`。 + +传递给此调用的降级函数非常通用:它应该采用指定类型的操作(在本例中为 *Cast*)并返回另一个仅使用 TVM 理解的数据类型的操作。 + +通常,我们希望用户借助对外部库的调用,来对其自定义数据类型进行操作。在示例中,`myfloat` 库在函数 `FloatToCustom32` 中实现了从 `float` 到 32 位 `myfloat` 的转换。一般情况下,创建一个辅助函数 `create_lower_func(...)`,它的作用是:给定一个字典,它将给定的 `Call`的操作,替换为基于操作和位宽的适当函数名称。它还通过将自定义数据类型存储在适当宽度的不透明 `uint` 中,从而删除自定义数据类型的使用;在我们的例子中,如 `uint32_t`。有关更多信息,参阅 [源代码](https://github.com/apache/tvm/blob/main/python/tvm/target/datatype.py)。 + +``` python +# 现在重新尝试运行程序: +try: + with tvm.transform.PassContext(config={"tir.disable_vectorize": True}): + z_output_myfloat = relay.create_executor("graph", mod=module).evaluate()(x_input, y_input) + print("z: {}".format(z_output_myfloat)) +except tvm.TVMError as e: + # 打印最后一行错误 + print(str(e).split("\n")[-1]) +``` + +输出结果: + +``` bash +Check failed: (lower) is false: Add lowering function for target llvm type 150 not found +``` + +新报错提示无法找到 `Add` 降级函数,这并不是坏事儿,这表明错误与 `Cast`无关!接下来只需要在程序中为其他操作注册降级函数。 + +注意,对于 `Add`,`create_lower_func` 接受一个键(key)是整数的字典。对于 `Cast` 操作,需要一个 2 元组来指定 `src_bit_length` 和 `dest_bit_length`,对于其他操作,操作数之间的位长度相同,因此只需要一个整数来指定 `bit_length`。 + +``` python +tvm.target.datatype.register_op( + tvm.target.datatype.create_lower_func({32: "Custom32Add"}), + "Add", + "llvm", + "myfloat", +) +tvm.target.datatype.register_op( + tvm.target.datatype.create_lower_func({(32, 32): "Custom32ToFloat"}), + "Cast", + "llvm", + "myfloat", + "float", +) + +# 现在,可以正常运行程序了。 +with tvm.transform.PassContext(config={"tir.disable_vectorize": True}): + z_output_myfloat = relay.create_executor(mod=module).evaluate()(x_input, y_input) +print("z: {}".format(z_output_myfloat)) + +print("x:\t\t{}".format(x_input)) +print("y:\t\t{}".format(y_input)) +print("z (float32):\t{}".format(z_output)) +print("z (myfloat32):\t{}".format(z_output_myfloat)) + +# 或许正如预期的那样,``myfloat32`` 结果和 ``float32`` 是完全一样的! +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +z: [0.7996937 1.168008 1.4516819] +x: [0.51729786 0.9469626 0.7654598 ] +y: [0.28239584 0.22104536 0.6862221 ] +z (float32): [0.7996937 1.168008 1.4516819] +z (myfloat32): [0.7996937 1.168008 1.4516819] +``` + +## 使用自定义数据类型运行模型 + +首先选择要使用 myfloat 运行的模型,本示例中,我们使用的是 [Mobilenet](https://arxiv.org/abs/1704.04861)。选择 Mobilenet 是因为它足够小。在 Bring Your Own Datatypes 框架的这个 alpha 状态下,还没有为运行自定义数据类型的软件仿真实现任何软件优化;由于多次调用数据类型仿真库,导致性能不佳。 + +首先定义两个辅助函数,获取 mobilenet 模型和猫图像。 + +``` python +def get_mobilenet(): + dshape = (1, 3, 224, 224) + from mxnet.gluon.model_zoo.vision import get_model + + block = get_model("mobilenet0.25", pretrained=True) + shape_dict = {"data": dshape} + return relay.frontend.from_mxnet(block, shape_dict) + +def get_cat_image(): + from tvm.contrib.download import download_testdata + from PIL import Image + + url = "https://gist.githubusercontent.com/zhreshold/bcda4716699ac97ea44f791c24310193/raw/fa7ef0e9c9a5daea686d6473a62aacd1a5885849/cat.png" + dst = "cat.png" + real_dst = download_testdata(url, dst, module="data") + img = Image.open(real_dst).resize((224, 224)) + # CoreML's standard model image format is BGR + img_bgr = np.array(img)[:, :, ::-1] + img = np.transpose(img_bgr, (2, 0, 1))[np.newaxis, :] + return np.asarray(img, dtype="float32") + +module, params = get_mobilenet() +``` + +输出结果: + +``` bash +Downloading /workspace/.mxnet/models/mobilenet0.25-9f83e440.zipe0e3327d-26bc-4c47-aed4-734a16b0a3f8 from https://apache-mxnet.s3-accelerate.dualstack.amazonaws.com/gluon/models/mobilenet0.25-9f83e440.zip... +``` + +用原生 TVM 很容易执行 MobileNet: + +``` python +ex = tvm.relay.create_executor("graph", mod=module, params=params) +input = get_cat_image() +result = ex.evaluate()(input).numpy() +# 打印前 10 个元素 +print(result.flatten()[:10]) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +[ -7.5350165 2.0368009 -12.706646 -5.63786 -12.684058 4.0723605 + 2.618876 3.4049501 -9.867913 -24.53311 ] +``` + +若要更改模型在内部使用 myfloat,需要转换网络。为此首先定义一个函数来帮助转换张量: + +``` python +def convert_ndarray(dst_dtype, array): + """Converts an NDArray into the specified datatype""" + x = relay.var("x", shape=array.shape, dtype=str(array.dtype)) + cast = relay.Function([x], x.astype(dst_dtype)) + with tvm.transform.PassContext(config={"tir.disable_vectorize": True}): + return relay.create_executor("graph").evaluate(cast)(array) +``` + +为了实际转换整个网络,我们在 Relay 中编写了 [一个 pass](https://github.com/gussmith23/tvm/blob/ea174c01c54a2529e19ca71e125f5884e728da6e/python/tvm/relay/frontend/change_datatype.py#L21),它简单地将模型中的所有节点转换为使用新的数据类型。 + +``` python +from tvm.relay.frontend.change_datatype import ChangeDatatype + +src_dtype = "float32" +dst_dtype = "custom[myfloat]32" + +module = relay.transform.InferType()(module) + +# 目前,自定义数据类型仅在预先运行 simple_inference 时才有效 +module = tvm.relay.transform.SimplifyInference()(module) + +# 在更改数据类型之前运行类型推断 +module = tvm.relay.transform.InferType()(module) + +# 将数据类型从 float 更改为 myfloat 并重新推断类型 +cdtype = ChangeDatatype(src_dtype, dst_dtype) +expr = cdtype.visit(module["main"]) +module = tvm.relay.transform.InferType()(module) + +# 转换参数: +params = {k: convert_ndarray(dst_dtype, v) for k, v in params.items()} + +# 还需要转换输入: +input = convert_ndarray(dst_dtype, input) + +# 最后,可以尝试运行转换后的模型: +try: + # 向量化不是用自定义数据类型实现的。 + with tvm.transform.PassContext(config={"tir.disable_vectorize": True}): + result_myfloat = tvm.relay.create_executor("graph", mod=module).evaluate(expr)( + input, **params + ) +except tvm.TVMError as e: + print(str(e).split("\n")[-1]) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " + Check failed: (lower) is false: Intrinsic lowering function for target llvm, intrinsic name tir.sqrt, type 150 not found +``` + +尝试运行模型时,会收到一个熟悉的报错,提示需要为 myfloat 注册更多函数。 + +因为这是一个神经网络,所以需要更多的操作。下面注册所有需要的函数: + +``` python +tvm.target.datatype.register_op( + tvm.target.datatype.create_lower_func({32: "FloatToCustom32"}), + "FloatImm", + "llvm", + "myfloat", +) + +tvm.target.datatype.register_op( + tvm.target.datatype.lower_ite, "Call", "llvm", "myfloat", intrinsic_name="tir.if_then_else" +) + +tvm.target.datatype.register_op( + tvm.target.datatype.lower_call_pure_extern, + "Call", + "llvm", + "myfloat", + intrinsic_name="tir.call_pure_extern", +) + +tvm.target.datatype.register_op( + tvm.target.datatype.create_lower_func({32: "Custom32Mul"}), + "Mul", + "llvm", + "myfloat", +) +tvm.target.datatype.register_op( + tvm.target.datatype.create_lower_func({32: "Custom32Div"}), + "Div", + "llvm", + "myfloat", +) + +tvm.target.datatype.register_op( + tvm.target.datatype.create_lower_func({32: "Custom32Sqrt"}), + "Call", + "llvm", + "myfloat", + intrinsic_name="tir.sqrt", +) + +tvm.target.datatype.register_op( + tvm.target.datatype.create_lower_func({32: "Custom32Sub"}), + "Sub", + "llvm", + "myfloat", +) + +tvm.target.datatype.register_op( + tvm.target.datatype.create_lower_func({32: "Custom32Exp"}), + "Call", + "llvm", + "myfloat", + intrinsic_name="tir.exp", +) + +tvm.target.datatype.register_op( + tvm.target.datatype.create_lower_func({32: "Custom32Max"}), + "Max", + "llvm", + "myfloat", +) + +tvm.target.datatype.register_min_func( + tvm.target.datatype.create_min_lower_func({32: "MinCustom32"}, "myfloat"), + "myfloat", +) +``` + +注意,我们使用的是:`register_min_func` 和 `create_min_lower_func`。 + +`register_min_func` 接收一个整数 `num_bits` 作为位长,然后返回一个表示最小有限可表示值的操作,这个值是具有指定位长的自定义数据类型。 + +与 `register_op` 和 `create_lower_func` 类似,`create_min_lower_func` 处理通过调用一个外部库,实现最小可表示的自定义数据类型值的一般情况。 + +接下来运行模型: + +``` python +# 向量化不是用自定义数据类型实现的。 +with tvm.transform.PassContext(config={"tir.disable_vectorize": True}): + result_myfloat = relay.create_executor(mod=module).evaluate(expr)(input, **params) + result_myfloat = convert_ndarray(src_dtype, result_myfloat).numpy() + # 打印前 10 个元素 + print(result_myfloat.flatten()[:10]) + +# 再次注意,使用 32 位 myfloat 的输出与 32 位浮点数完全相同, +# 因为 myfloat 就是一个浮点数! +np.testing.assert_array_equal(result, result_myfloat) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +[ -7.5350165 2.0368009 -12.706646 -5.63786 -12.684058 4.0723605 + 2.618876 3.4049501 -9.867913 -24.53311 ] +``` + +[下载 Python 源代码:bring_your_own_datatypes.py](https://tvm.apache.org/docs/_downloads/ee99205e9f2e4f54c0fb7925008a5354/bring_your_own_datatypes.py) + +[下载 Jupyter Notebook:bring_your_own_datatypes.ipynb](https://tvm.apache.org/docs/_downloads/b11795df0596a55e4982bf895d0c8c38/bring_your_own_datatypes.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/extend/_category_.json b/versioned_docs/version-0.12.0/how_to/extend/_category_.json new file mode 100644 index 00000000..aee7c4d2 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/extend/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 190 +} diff --git a/versioned_docs/version-0.12.0/how_to/extend/index.md b/versioned_docs/version-0.12.0/how_to/extend/index.md new file mode 100644 index 00000000..882821f6 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/extend/index.md @@ -0,0 +1,10 @@ +--- +title: 扩展 TVM +--- + +TVM 是一个可扩展的开发平台,它有很多可用的入口点,包括引入新数据类型,以及添加更低级别的自定义优化 pass 的选项。以下介绍了扩展 TVM 的一些方法。 + +* [编写自定义 Pass](writing_pass) +* [如何使用 TVM Pass Infra](pass_infra) +* [如何使用 TVM Pass Instrument](pass_instrument) +* [向 TVM 中添加自定义数据类型](datatypes) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/microtvm/01-aot.md b/versioned_docs/version-0.12.0/how_to/microtvm/01-aot.md new file mode 100644 index 00000000..8389cb4f --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/microtvm/01-aot.md @@ -0,0 +1,160 @@ +--- +title: microTVM 主机驱动的 AoT +--- + +# microTVM 主机驱动的 AoT + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/work_with_microtvm/micro_aot.html#sphx-glr-download-how-to-work-with-microtvm-micro-aot-py) 下载完整的示例代码 +::: + +**作者**:[Mehrdad Hessar](https://github.com/mehrdadh),[Alan MacDonald](https://github.com/alanmacd) + +本教程展示了 microTVM(使用 TFLite 模型)主机驱动的 AoT 编译。与 GraphExecutor 相比,AoTExecutor 减少了运行时解析图的开销。此外,我们可以通过提前编译更好地进行内存管理。本教程可以使用 C 运行时(CRT)在 x86 CPU 上执行,也可以在 Zephyr 支持的微控制器/板上的 Zephyr 平台上执行。 + +``` python +import numpy as np +import pathlib +import json +import os + +import tvm +from tvm import relay +from tvm.relay.backend import Executor, Runtime +from tvm.contrib.download import download_testdata +``` + +## 导入 TFLite 模型 + +首先,下载并导入 Keyword Spotting TFLite 模型。该模型最初来自 [MLPerf Tiny 仓库](https://github.com/mlcommons/tiny)。使用 [Google 提供的 KWS 数据集的样本](https://ai.googleblog.com/2017/08/launching-speech-commands-dataset.html) 来测试这个模型。 + +**注意:**默认情况下,本教程使用 CRT 在 x86 CPU 上运行,若要在 Zephyr 平台上运行,需要导出 *TVM_MICRO_USE_HW* 环境变量。 + +``` python +use_physical_hw = bool(os.getenv("TVM_MICRO_USE_HW")) +MODEL_URL = "https://github.com/tlc-pack/web-data/raw/main/testdata/microTVM/model/keyword_spotting_quant.tflite" +MODEL_PATH = download_testdata(MODEL_URL, "keyword_spotting_quant.tflite", module="model") +SAMPLE_URL = "https://github.com/tlc-pack/web-data/raw/main/testdata/microTVM/data/keyword_spotting_int8_6.pyc.npy" +SAMPLE_PATH = download_testdata(SAMPLE_URL, "keyword_spotting_int8_6.pyc.npy", module="data") + +tflite_model_buf = open(MODEL_PATH, "rb").read() +try: + import tflite + + tflite_model = tflite.Model.GetRootAsModel(tflite_model_buf, 0) +except AttributeError: + import tflite.Model + + tflite_model = tflite.Model.Model.GetRootAsModel(tflite_model_buf, 0) + +input_shape = (1, 49, 10, 1) +INPUT_NAME = "input_1" +relay_mod, params = relay.frontend.from_tflite( + tflite_model, shape_dict={INPUT_NAME: input_shape}, dtype_dict={INPUT_NAME: "int8"} +) +``` + +## 定义 target + +接下来定义 target、runtime 和 executor。本教程将详细介绍使用 AOT 主机驱动的执行器。这里使用的主机微 target,它使用 CRT runtime 在 x86 CPU 上运行模型,或者在 qemu_x86 模拟器单板上运行带有 Zephyr 平台的模型。对于物理微控制器,获取物理单板(例如 nucleo_l4r5zi)的 target 模型,并将其传递给 *tvm.target.target.micro*,从而创建完整的微目标。 + +``` python +# 使用 C runtime(crt),并通过将 system-lib 设置为 True 来启用静态链接 +RUNTIME = Runtime("crt", {"system-lib": True}) + +# 在主机上模拟一个微控制器。使用来自 `src/runtime/crt/host/main.cc [https://github.com/apache/tvm/blob/main/src/runtime/crt/host/main.cc](https://github.com/apache/tvm/blob/main/src/runtime/crt/host/main.cc)`_ 的 main()。 +# 若要使用物理硬件,请将「host」替换为与你的硬件匹配的内容。 +TARGET = tvm.target.target.micro("host") + +# 使用 AOT 执行器,而非计算图或是虚拟机执行器。不要使用未打包的 API 或 C 调用风格。 +EXECUTOR = Executor("aot") + +if use_physical_hw: + boards_file = pathlib.Path(tvm.micro.get_microtvm_template_projects("zephyr")) / "boards.json" + with open(boards_file) as f: + boards = json.load(f) + BOARD = os.getenv("TVM_MICRO_BOARD", default="nucleo_l4r5zi") + TARGET = tvm.target.target.micro(boards[BOARD]["model"]) +``` + +## 编译模型 + +接下来为 target 编译模型: + +``` python +with tvm.transform.PassContext(opt_level=3, config={"tir.disable_vectorize": True}): + module = tvm.relay.build( + relay_mod, target=TARGET, params=params, runtime=RUNTIME, executor=EXECUTOR + ) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:267: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +## 创建一个 microTVM 项目 + +将编译好的模型作为 IRModule,然后创建一个固件项目,将编译好的模型与 microTVM 配合使用。可以借助 Project API 来实现。我们定义了 CRT 和 Zephyr microTVM 模板项目,分别用于 x86 CPU 和 Zephyr 板。 + +``` python +template_project_path = pathlib.Path(tvm.micro.get_microtvm_template_projects("crt")) +project_options = {} # 可以用选项通过 TVM 提供特定于平台的选项。 + +if use_physical_hw: + template_project_path = pathlib.Path(tvm.micro.get_microtvm_template_projects("zephyr")) + project_options = {"project_type": "host_driven", "zephyr_board": BOARD} + +temp_dir = tvm.contrib.utils.tempdir() +generated_project_dir = temp_dir / "project" +project = tvm.micro.generate_project( + template_project_path, module, generated_project_dir, project_options +) +``` + +## 构建、烧录和执行模型 + +接下来构建 microTVM 项目,并将其烧录。Flash 步骤是特定于物理微控制器的。若它通过主机 main.cc 来模拟微控制器,或是将 Zephyr 仿真板作为 target,则会跳过该步骤。接下来,为模型输出定义标签,并使用期望值为 6 的样本(label: left)执行模型。 + +``` python +project.build() +project.flash() + +labels = [ + "_silence_", + "_unknown_", + "yes", + "no", + "up", + "down", + "left", + "right", + "on", + "off", + "stop", + "go", +] +with tvm.micro.Session(project.transport()) as session: + aot_executor = tvm.runtime.executor.aot_executor.AotModule(session.create_aot_executor()) + sample = np.load(SAMPLE_PATH) + aot_executor.get_input(INPUT_NAME).copyfrom(sample) + aot_executor.run() + result = aot_executor.get_output(0).numpy() + print(f"Label is `{labels[np.argmax(result)]}` with index `{np.argmax(result)}`") +# +# 输出: +# label 为 `left`,其 index 为 `6` +# +``` + +输出结果: + +``` bash +Label is `left` with index `6` +``` + +[下载 Python 源代码:micro_aot.py](https://tvm.apache.org/docs/_downloads/f8a7209a0e66b246185bfc41bbc82f54/micro_aot.py) + +[下载 Jupyter Notebook:micro_aot.ipynb](https://tvm.apache.org/docs/_downloads/c00933f3fbcf90c4f584d54607b33805/micro_aot.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/microtvm/02-autotune_microtvm.md b/versioned_docs/version-0.12.0/how_to/microtvm/02-autotune_microtvm.md new file mode 100644 index 00000000..837a936c --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/microtvm/02-autotune_microtvm.md @@ -0,0 +1,278 @@ +--- +title: 使用 microTVM 进行自动调优 +--- + +# 使用 microTVM 进行自动调优 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/work_with_microtvm/micro_autotune.html#sphx-glr-download-how-to-work-with-microtvm-micro-autotune-py) 下载完整的示例代码 +::: + +**作者**:[Andrew Reusch](https://github.com/areusch), [Mehrdad Hessar](https://github.com/mehrdadh) + +本教程介绍如何用 C runtime 自动调优模型。 + +``` python +import os +import json +import numpy as np +import pathlib + +import tvm +from tvm.relay.backend import Runtime + +use_physical_hw = bool(os.getenv("TVM_MICRO_USE_HW")) +``` + +## 定义模型 + +首先在 Relay 中定义一个要在设备上执行的模型,然后从 Relay 模型中创建一个 IRModule,并用随机数填充参数。 + +``` python +data_shape = (1, 3, 10, 10) +weight_shape = (6, 3, 5, 5) + +data = tvm.relay.var("data", tvm.relay.TensorType(data_shape, "float32")) +weight = tvm.relay.var("weight", tvm.relay.TensorType(weight_shape, "float32")) + +y = tvm.relay.nn.conv2d( + data, + weight, + padding=(2, 2), + kernel_size=(5, 5), + kernel_layout="OIHW", + out_dtype="float32", +) +f = tvm.relay.Function([data, weight], y) + +relay_mod = tvm.IRModule.from_expr(f) +relay_mod = tvm.relay.transform.InferType()(relay_mod) + +weight_sample = np.random.rand( + weight_shape[0], weight_shape[1], weight_shape[2], weight_shape[3] +).astype("float32") +params = {"weight": weight_sample} +``` + +## 定义 target + +下面定义描述执行环境的 TVM target,它与其他 microTVM 教程中的 target 定义非常相似。不同之处是用 C Runtime 来生成模型。 + +在物理硬件上运行时,选择一个 target 和一个描述硬件的单板。在本教程的 PLATFORM 列表中可以选择多个硬件目标。运行本教程时用 –platform 参数来选择平台。 + +``` python +RUNTIME = Runtime("crt", {"system-lib": True}) +TARGET = tvm.target.target.micro("host") + +# 为物理硬件编译 +# -------------------------------------------------------------------------- +# 在物理硬件上运行时,选择描述硬件的 TARGET 和 BOARD。 +# 下面的示例中选择 STM32L4R5ZI Nucleo。 +if use_physical_hw: + boards_file = pathlib.Path(tvm.micro.get_microtvm_template_projects("zephyr")) / "boards.json" + with open(boards_file) as f: + boards = json.load(f) + + BOARD = os.getenv("TVM_MICRO_BOARD", default="nucleo_l4r5zi") + TARGET = tvm.target.target.micro(boards[BOARD]["model"]) +``` + +## 提取调优任务 + +并非上面打印的 Relay 程序中的所有算子都可以调优,有些算子不是很重要,所以只定义了一个实现;其他的作为调优任务没有意义。用 extract_from_program,可以生成可调优任务列表。 + +因为任务提取涉及到运行编译器,所以首先配置编译器的转换 pass;之后在自动调优过程中应用相同的配置。 + +``` python +pass_context = tvm.transform.PassContext(opt_level=3, config={"tir.disable_vectorize": True}) +with pass_context: + tasks = tvm.autotvm.task.extract_from_program(relay_mod["main"], {}, TARGET) +assert len(tasks) > 0 +``` + +## 配置 microTVM + +自动调优前,要定义一个模块加载器,然后将它传递给 *tvm.autotvm.LocalBuilder*。创建一个 *tvm.autotvm.LocalBuilder*,并用 builder 和 runner 为自动调优器生成多个测试值。 + +本教程中可以选择用 x86 主机作为示例,或用来自 Zephyr RTOS 的不同 targets。若用 x86,则传递 *–platform=host*,也可以从 *PLATFORM* 列表中选择其他选项。 + +``` python +module_loader = tvm.micro.AutoTvmModuleLoader( + template_project_dir=pathlib.Path(tvm.micro.get_microtvm_template_projects("crt")), + project_options={"verbose": False}, +) +builder = tvm.autotvm.LocalBuilder( + n_parallel=1, + build_kwargs={"build_option": {"tir.disable_vectorize": True}}, + do_fork=True, + build_func=tvm.micro.autotvm_build_func, + runtime=RUNTIME, +) +runner = tvm.autotvm.LocalRunner(number=1, repeat=1, timeout=100, module_loader=module_loader) + +measure_option = tvm.autotvm.measure_option(builder=builder, runner=runner) + +# 为物理硬件编译 +if use_physical_hw: + module_loader = tvm.micro.AutoTvmModuleLoader( + template_project_dir=pathlib.Path(tvm.micro.get_microtvm_template_projects("zephyr")), + project_options={ + "zephyr_board": BOARD, + "west_cmd": "west", + "verbose": False, + "project_type": "host_driven", + }, + ) + builder = tvm.autotvm.LocalBuilder( + n_parallel=1, + build_kwargs={"build_option": {"tir.disable_vectorize": True}}, + do_fork=False, + build_func=tvm.micro.autotvm_build_func, + runtime=RUNTIME, + ) + runner = tvm.autotvm.LocalRunner(number=1, repeat=1, timeout=100, module_loader=module_loader) + + measure_option = tvm.autotvm.measure_option(builder=builder, runner=runner) +``` + +## 运行自动调优 + +下面在 microTVM 设备上对每个提取的任务单独运行自动调优。 + +``` python +autotune_log_file = pathlib.Path("microtvm_autotune.log.txt") +if os.path.exists(autotune_log_file): + os.remove(autotune_log_file) + +num_trials = 10 +for task in tasks: + tuner = tvm.autotvm.tuner.GATuner(task) + tuner.tune( + n_trial=num_trials, + measure_option=measure_option, + callbacks=[ + tvm.autotvm.callback.log_to_file(str(autotune_log_file)), + tvm.autotvm.callback.progress_bar(num_trials, si_prefix="M"), + ], + si_prefix="M", + ) +``` + +## 为未调优的程序计时 + +为了方便比较,编译并运行不实施任何自动调优 schedule 的计算图,TVM 会为每个算子选择一个随机调优的实现,其性能不如调优的算子。 + +``` python +with pass_context: + lowered = tvm.relay.build(relay_mod, target=TARGET, runtime=RUNTIME, params=params) + +temp_dir = tvm.contrib.utils.tempdir() +project = tvm.micro.generate_project( + str(tvm.micro.get_microtvm_template_projects("crt")), + lowered, + temp_dir / "project", + {"verbose": False}, +) + +# 为物理硬件编译 +if use_physical_hw: + temp_dir = tvm.contrib.utils.tempdir() + project = tvm.micro.generate_project( + str(tvm.micro.get_microtvm_template_projects("zephyr")), + lowered, + temp_dir / "project", + { + "zephyr_board": BOARD, + "west_cmd": "west", + "verbose": False, + "project_type": "host_driven", + }, + ) + +project.build() +project.flash() +with tvm.micro.Session(project.transport()) as session: + debug_module = tvm.micro.create_local_debug_executor( + lowered.get_graph_json(), session.get_system_lib(), session.device + ) + debug_module.set_input(**lowered.get_params()) + print("########## Build without Autotuning ##########") + debug_module.run() + del debug_module +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +########## Build without Autotuning ########## +Node Name Ops Time(us) Time(%) Shape Inputs Outputs Measurements(us) +--------- --- -------- ------- ----- ------ ------- ---------------- +tvmgen_default_fused_nn_contrib_conv2d_NCHWc tvmgen_default_fused_nn_contrib_conv2d_NCHWc 310.0 98.73 (1, 2, 10, 10, 3) 2 1 [310.0] +tvmgen_default_fused_layout_transform_1 tvmgen_default_fused_layout_transform_1 3.031 0.965 (1, 6, 10, 10) 1 1 [3.031] +tvmgen_default_fused_layout_transform tvmgen_default_fused_layout_transform 0.958 0.305 (1, 1, 10, 10, 3) 1 1 [0.958] +Total_time - 313.988 - - - - - +``` + +## 为调优程序计时 + +自动调优完成后,用 Debug Runtime 为整个程序的执行计时: + +``` python +with tvm.autotvm.apply_history_best(str(autotune_log_file)): + with pass_context: + lowered_tuned = tvm.relay.build(relay_mod, target=TARGET, runtime=RUNTIME, params=params) + +temp_dir = tvm.contrib.utils.tempdir() +project = tvm.micro.generate_project( + str(tvm.micro.get_microtvm_template_projects("crt")), + lowered_tuned, + temp_dir / "project", + {"verbose": False}, +) + +# 为物理硬件编译 +if use_physical_hw: + temp_dir = tvm.contrib.utils.tempdir() + project = tvm.micro.generate_project( + str(tvm.micro.get_microtvm_template_projects("zephyr")), + lowered_tuned, + temp_dir / "project", + { + "zephyr_board": BOARD, + "west_cmd": "west", + "verbose": False, + "project_type": "host_driven", + }, + ) + +project.build() +project.flash() +with tvm.micro.Session(project.transport()) as session: + debug_module = tvm.micro.create_local_debug_executor( + lowered_tuned.get_graph_json(), session.get_system_lib(), session.device + ) + debug_module.set_input(**lowered_tuned.get_params()) + print("########## Build with Autotuning ##########") + debug_module.run() + del debug_module +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +########## Build with Autotuning ########## +Node Name Ops Time(us) Time(%) Shape Inputs Outputs Measurements(us) +--------- --- -------- ------- ----- ------ ------- ---------------- +tvmgen_default_fused_nn_contrib_conv2d_NCHWc tvmgen_default_fused_nn_contrib_conv2d_NCHWc 193.2 98.657 (1, 6, 10, 10, 1) 2 1 [193.2] +tvmgen_default_fused_layout_transform_1 tvmgen_default_fused_layout_transform_1 1.778 0.908 (1, 6, 10, 10) 1 1 [1.778] +tvmgen_default_fused_layout_transform tvmgen_default_fused_layout_transform 0.851 0.435 (1, 3, 10, 10, 1) 1 1 [0.851] +Total_time - 195.83 - - - - - +``` + +[下载 Python 源代码:micro_autotune.py](https://tvm.apache.org/docs/_downloads/9ccca8fd489a1486ac71b55a55c320c5/micro_autotune.py) + +[下载 Jupyter notebook:micro_autotune.ipynb](https://tvm.apache.org/docs/_downloads/f83ba3df2d52f9b54cf141114359481a/micro_autotune.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/microtvm/03-tvm_arm.md b/versioned_docs/version-0.12.0/how_to/microtvm/03-tvm_arm.md new file mode 100644 index 00000000..492d7344 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/microtvm/03-tvm_arm.md @@ -0,0 +1,449 @@ +--- +title: 在支持 CMSIS-NN 的 Arm(R) Cortex(R)-M55 CPU 和 Ethos(TM)-U55 NPU 裸机上运行 TVM +--- + +# 在支持 CMSIS-NN 的 Arm(R) Cortex(R)-M55 CPU 和 Ethos(TM)-U55 NPU 裸机上运行 TVM + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/work_with_microtvm/micro_ethosu.html#sphx-glr-download-how-to-work-with-microtvm-micro-ethosu-py) 下载完整的示例代码 +::: + +**作者**:[Grant Watson](https://github.com/grant-arm) + +本节使用示例说明如何使用 TVM 在带有 CMSIS-NN 的 Arm(R) Cortex(R)-M55 CPU 和 Ethos(TM)-U55 NPU 的裸机上运行模型。Cortex(R)-M55 是一款用于嵌入式设备的小型低功耗 CPU。CMSIS-NN 是针对 Arm(R) Cortex(R)-M CPU 优化的内核集合。Ethos(TM)-U55 是一种 microNPU,专门为在资源受限的嵌入式设备中,加速机器学习的推理而设计。 + +在没有 Cortex(R)-M55 和 Ethos(TM)-U55 开发板的情况下,若要运行 demo 程序,可在固定虚拟平台(FVP)上运行。基于 Arm(R) Corstone(TM)-300 软件的 FVP,对包含 Cortex(R)-M55 和 Ethos(TM)-U55 的硬件系统进行建模。它对于软件开发非常友好。 + +本教程将编译一个 MobileNet v1 模型并使用 TVM 尽可能将算子迁移到 Ethos(TM)-U55。 + +## 获取 TVM + +为平台获取 TVM,请访问 https://tlcpack.ai/ 并按照说明进行操作。正确安装 TVM 后,可以通过命令行访问 `tvmc`。 + +在命令行上键入 `tvmc` 应显示以下内容: + +``` bash +usage: tvmc [-h] [-v] [--version] {tune,compile,run} ... + +TVM compiler driver + +optional arguments: + -h, --help show this help message and exit + -v, --verbose increase verbosity + --version print the version and exit + +commands: + {tune,compile,run} + tune auto-tune a model + compile compile a model. + run run a compiled module + +TVMC - TVM driver command-line interface +``` + +## 安装额外的 Python 依赖 + +运行 demo 需要一些额外的 Python 包。通过下面的 requirements.txt 文件,来安装这些额外的 Python 包: + +*requirements.txt*[¶](#requirements-txt) + +``` text + attrs==21.2.0 + cloudpickle==2.0.0 + decorator==5.1.0 + ethos-u-vela==3.2.0 + flatbuffers==1.12 + lxml==4.6.3 + nose==1.3.7 + numpy==1.19.5 + Pillow==8.3.2 + psutil==5.8.0 + scipy==1.5.4 + synr==0.4 + tflite==2.4.0 + tornado==6.1 +``` + +运行以下命令来安装这些软件包: + +``` bash +pip install -r requirements.txt +``` + +## 获取模型 + +本教程使用 MobileNet v1 模型(一种卷积神经网络,旨在对图像进行分类),该模型针对边缘设备进行了优化。要使用的模型已经预训练过,它可以可以识别 1001 个类别。该网络的输入图像大小为 224x224,任何输入图像在使用之前都需要调整到这个尺寸。 + +本教程使用 TFLite 格式的模型。 + +``` bash +mkdir -p ./build +cd build +wget https://storage.googleapis.com/download.tensorflow.org/models/mobilenet_v1_2018_08_02/mobilenet_v1_1.0_224_quant.tgz +gunzip mobilenet_v1_1.0_224_quant.tgz +tar xvf mobilenet_v1_1.0_224_quant.tar +``` + +## 为具有 CMSIS-NN 的 Arm(R) Cortex(R)-M55 CPU 和 Ethos(TM)-U55 NPU 设备编译模型 + +下载 MobileNet v1 模型后,下一步是用 tvmc compile 进行编译。编译过程中得到的输出是模型的 TAR 包,该模型编译为 target 平台的模型库格式(MLF),能够用 TVM runtime 在 target 设备上运行该模型。 + +``` bash +tvmc compile --target=ethos-u,cmsis-nn,c \ + --target-ethos-u-accelerator_config=ethos-u55-256 \ + --target-cmsis-nn-mcpu=cortex-m55 \ + --target-c-mcpu=cortex-m55 \ + --runtime=crt \ + --executor=aot \ + --executor-aot-interface-api=c \ + --executor-aot-unpacked-api=1 \ + --pass-config tir.usmp.enable=1 \ + --pass-config tir.usmp.algorithm=hill_climb \ + --pass-config tir.disable_storage_rewrite=1 \ + --pass-config tir.disable_vectorize=1 \ + ./mobilenet_v1_1.0_224_quant.tflite \ + --output-format=mlf +``` + +:::note +tvmc 编译参数说明: + +* `--target=ethos-u,cmsis-nn,c`:在可能的情况下将算子迁移到 microNPU,回退到 CMSIS-NN 并最终生成 microNPU 不支持的算子的 C 代码。 +* `--target-ethos-u-accelerator_config=ethos-u55-256`:指定 microNPU 配置。 +* `--target-c-mcpu=cortex-m55`:针对 Cortex(R)-M55 进行交叉编译。 +* `--runtime=crt`:生成粘合代码以支持算子使用 C runtime。 +* `--executor=aot`:使用 Ahead Of Time 编译而非 Graph Executor。 +* `--executor-aot-interface-api=c`:生成一个 C 风格的接口,其结构专为在边界集成到 C 应用程序而设计。 +* `--executor-aot-unpacked-api=1`:在内部使用非压缩的 API。 +* `--pass-config tir.usmp.enable=1`:启用统一静态内存规划 +* `--pass-config tir.usmp.algorithm=hill_climb`:对 USMP 使用爬山算法 +* `--pass-config tir.disable_storage_rewrite=1`: 禁用存储重写 +* `--pass-config tir.disable_vectorize=1`:禁用向量化,因为 C 中没有标准的向量化类型。 +* `./mobilenet_v1_1.0_224_quant.tflite`:正在编译的 TFLite 模型。 +* `--output-format=mlf`:以模型库格式生成输出。 +::: + +:::note +**若不想用 microNPU 进行迁移** + +仅适用于 CMSIS-NN 的算子: + +* 使用 `--target=cmsis-nn,c` 代替 `--target=ethos-u,cmsis-nn,c` +* 删除 microNPU 配置参数 `--target-ethos-u-accelerator_config=ethos-u55-256` +::: + +## 将生成的代码解压到当前目录 + +``` bash +tar xvf module.tar +``` + +## 获取 ImageNet 标签 + +在图像上运行 MobileNet v1 时,输出结果是 0 到 1000 的索引。为了使应用程序对用户更加友好,将显示相关标签,而非仅显示类别索引。将图像标签下载到一个文本文件中,然后用 Python 脚本将它们包含在 C 程序中。 + +``` bash +curl -sS https://raw.githubusercontent.com/tensorflow/tensorflow/master/tensorflow/lite/java/demo/app/src/main/assets/labels_mobilenet_quant_v1_224.txt \ +-o ./labels_mobilenet_quant_v1_224.txt +``` + +## 获取输入图像 + +本教程使用猫的图像作为输入,也可以自行替换为其他图像。 + +![图片](https://s3.amazonaws.com/model-server/inputs/kitten.jpg) + +将图像下载到构建目录中,下一步用 Python 脚本,将图像转换为 C 头文件中的字节数组。 + +``` bash +curl -sS https://s3.amazonaws.com/model-server/inputs/kitten.jpg -o ./kitten.jpg +``` + +## 预处理图像 + +以下脚本将在 src 目录中创建 2 个 C 头文件: + +* `inputs.h` - 作为脚本参数提供的图像将被转换为整数数组,以输入到 MobileNet v1 模型。 +* `outputs.h` - 用一个全零的整数数组,为推理的输出保留 1001 个整数值。 + +*convert_image.py*[¶](#convert-image-py) + +``` python +#!python ./convert_image.py +import os +import pathlib +import re +import sys +from PIL import Image +import numpy as np + +def create_header_file(name, section, tensor_name, tensor_data, output_path): + """ + 这个函数产生一个头文件,包含 numpy 数组提供的数据。 + """ + file_path = pathlib.Path(f"{output_path}/" + name).resolve() + # 创建带有 npy_data 的头文件作为 C 数组 + raw_path = file_path.with_suffix(".h").resolve() + with open(raw_path, "w") as header_file: + header_file.write( + "#include \n" + + f"const size_t {tensor_name}_len = {tensor_data.size};\n" + + f'uint8_t {tensor_name}[] __attribute__((section("{section}"), aligned(16))) = "' + ) + data_hexstr = tensor_data.tobytes().hex() + for i in range(0, len(data_hexstr), 2): + header_file.write(f"\x{data_hexstr[i:i+2]}") + header_file.write('";\n\n') + +def create_headers(image_name): + """ + 此函数为运行推理所需的输入和输出数组生成 C 头文件 + """ + img_path = os.path.join("./", f"{image_name}") + + # 将图像大小调整为 224x224 + resized_image = Image.open(img_path).resize((224, 224)) + img_data = np.asarray(resized_image).astype("float32") + + # 将输入转换为 NCHW + img_data = np.transpose(img_data, (2, 0, 1)) + + # 创建输入头文件 + input_data = img_data.astype(np.uint8) + create_header_file("inputs", "ethosu_scratch", "input", input_data, "./include") + # 创建输出头文件 + output_data = np.zeros([1001], np.uint8) + create_header_file( + "outputs", + "output_data_sec", + "output", + output_data, + "./include", + ) + +if __name__ == "__main__": + create_headers(sys.argv[1]) +``` + +用以下命令行运行脚本: + +``` bash +python convert_image.py ./kitten.jpg +``` + +## 预处理标签 + +以下脚本将在 src 目录中创建一个 `labels.h` 头文件,之前下载的 labels.txt 文件会变成字符串数组。该数组将用于显示图像分类后为的标签。 + +*convert_labels.py*[¶](#convert-image) + +``` python +#!python ./convert_labels.py +import os +import pathlib +import sys + +def create_labels_header(labels_file, section, output_path): + """ + 此函数生成一个包含 ImageNet 标签的头文件作为字符串数组 + """ + labels_path = pathlib.Path(labels_file).resolve() + file_path = pathlib.Path(f"{output_path}/labels.h").resolve() + with open(labels_path) as f: + labels = f.readlines() + with open(file_path, "w") as header_file: + header_file.write(f'char* labels[] __attribute__((section("{section}"), aligned(16))) = {{') + for _, label in enumerate(labels): + header_file.write(f'"{label.rstrip()}",') + header_file.write("};\n") + +if __name__ == "__main__": + create_labels_header(sys.argv[1], "ethosu_scratch", "./include") +``` + +用以下命令行运行脚本: + +``` bash +python convert_labels.py +``` + +## 编写 demo + +下面的 C 程序会在之前下载并转换为整数数组的图像上,运行 MobileNet v1 模型的单个推理。由于该模型是以「ethos-u ...」为 target 编译的,因此需要迁移 Ethos(TM)-U55 NPU 支持的算子,以便进行加速。一旦应用程序构建并运行,测试图像应该被正确地归类为「tabby」,并且结果会显示在控制台上。这个文件应该放在 `./src`。 + +demo.c[¶](#demo-c) + +``` c + #include + #include + + #include "ethosu_mod.h" + #include "uart.h" + + // convert_image.py 和 convert_labels.py 生成的头文件 + #include "inputs.h" + #include "labels.h" + #include "outputs.h" + + int abs(int v) { return v * ((v > 0) - (v < 0)); } + + int main(int argc, char** argv) { + uart_init(); + printf("Starting Demo\n"); + EthosuInit(); + + printf("Allocating memory\n"); + StackMemoryManager_Init(&app_workspace, g_aot_memory, WORKSPACE_SIZE); + + printf("Running inference\n"); + struct tvmgen_default_outputs outputs = { + .output = output, + }; + struct tvmgen_default_inputs inputs = { + .input = input, + }; + struct ethosu_driver* driver = ethosu_reserve_driver(); + struct tvmgen_default_devices devices = { + .ethos_u = driver, + }; + tvmgen_default_run(&inputs, &outputs, &devices); + ethosu_release_driver(driver); + + // 计算最大值的索引 + uint8_t max_value = 0; + int32_t max_index = -1; + for (unsigned int i = 0; i < output_len; ++i) { + if (output[i] > max_value) { + max_value = output[i]; + max_index = i; + } + } + printf("The image has been classified as '%s'\n", labels[max_index]); + + // 当 FVP 在 UART 上接收到「EXITTHESIM」时,FVP 将关闭 + printf("EXITTHESIM\n"); + while (1 == 1) + ; + return 0; + } +``` + +此外,需要将 github 的这些头文件放入 `./include` 目录: + +[包含文件](https://github.com/apache/tvm/tree/main/apps/microtvm/ethosu/include) + +:::note +若要用 FreeRTOS 进行任务调度和队列,可以在 [demo_freertos.c](https://github.com/apache/tvm/blob/main/apps/microtvm/ethosu/src/demo_freertos.c) 找到示例程序。 +::: + +## 创建链接脚本(linker script) + +创建一个链接脚本,便于在下一节中构建应用程序时使用。链接脚本告诉链接器所有文件都放在内存中。下面的 corstone300.ld 链接脚本应该放在工作目录中。 + +查看 FVP 示例链接脚本 [corstone300.ld](https://github.com/apache/tvm/blob/main/apps/microtvm/ethosu/corstone300.ld) + +:::note +TVM 生成的代码会将模型权重和 Arm(R) Ethos(TM)-U55 命令流放在名为 `ethosu_scratch` 的部分中,对于 MobileNet v1 大小的模型,无法将权重和命令流全部放入 SRAM 的可用空间(因为 SRAM 空间十分有限)。因此,链接脚本将 `ethosu_scratch` 这部分放入 DRAM(DDR)中。 +::: + +:::note +在构建和运行应用程序之前,需要更新 PATH 环境变量,使其包含 cmake 3.19.5 和 FVP 的路径。例如,若已将它们安装在 `/opt/arm` 中,则执行以下操作: + +``` bash +export PATH=/opt/arm/FVP_Corstone_SSE-300_Ethos-U55/models/Linux64_GCC-6.4:/opt/arm/cmake/bin:$PATH +``` +::: + +## 使用 make 构建 demo + +接下来可以使用 make 构建 demo。在命令行运行 `make` 之前,应将 Makefile 移在工作目录中: + +Makefile 示例:[Makefile](https://github.com/apache/tvm/blob/main/apps/microtvm/ethosu/Makefile) + +:::note +**若使用的是 FreeRTOS,Makefile 会从指定的 FREERTOS_PATH 构建:** + +`make FREERTOS_PATH=` +::: + +## 运行 demo + +最后,使用以下命令在固定虚拟平台(Fixed Virtual Platform,简称 FVP)上运行 demo: + +``` bash +FVP_Corstone_SSE-300_Ethos-U55 -C cpu0.CFGDTCMSZ=15 \ +-C cpu0.CFGITCMSZ=15 -C mps3_board.uart0.out_file=\"-\" -C mps3_board.uart0.shutdown_tag=\"EXITTHESIM\" \ +-C mps3_board.visualisation.disable-visualisation=1 -C mps3_board.telnetterminal0.start_telnet=0 \ +-C mps3_board.telnetterminal1.start_telnet=0 -C mps3_board.telnetterminal2.start_telnet=0 -C mps3_board.telnetterminal5.start_telnet=0 \ +-C ethosu.extra_args="--fast" \ +-C ethosu.num_macs=256 ./build/demo +``` + +应在控制台窗口中看到以下输出: + +``` bash +telnetterminal0: Listening for serial connection on port 5000 +telnetterminal1: Listening for serial connection on port 5001 +telnetterminal2: Listening for serial connection on port 5002 +telnetterminal5: Listening for serial connection on port 5003 + + Ethos-U rev dedfa618 --- Jan 12 2021 23:03:55 + (C) COPYRIGHT 2019-2021 Arm Limited + ALL RIGHTS RESERVED + +Starting Demo +ethosu_init. base_address=0x48102000, fast_memory=0x0, fast_memory_size=0, secure=1, privileged=1 +ethosu_register_driver: New NPU driver at address 0x20000de8 is registered. +CMD=0x00000000 +Soft reset NPU +Allocating memory +Running inference +ethosu_find_and_reserve_driver - Driver 0x20000de8 reserved. +ethosu_invoke +CMD=0x00000004 +QCONFIG=0x00000002 +REGIONCFG0=0x00000003 +REGIONCFG1=0x00000003 +REGIONCFG2=0x00000013 +REGIONCFG3=0x00000053 +REGIONCFG4=0x00000153 +REGIONCFG5=0x00000553 +REGIONCFG6=0x00001553 +REGIONCFG7=0x00005553 +AXI_LIMIT0=0x0f1f0000 +AXI_LIMIT1=0x0f1f0000 +AXI_LIMIT2=0x0f1f0000 +AXI_LIMIT3=0x0f1f0000 +ethosu_invoke OPTIMIZER_CONFIG +handle_optimizer_config: +Optimizer release nbr: 0 patch: 1 +Optimizer config cmd_stream_version: 0 macs_per_cc: 8 shram_size: 48 custom_dma: 0 +Optimizer config Ethos-U version: 1.0.6 +Ethos-U config cmd_stream_version: 0 macs_per_cc: 8 shram_size: 48 custom_dma: 0 +Ethos-U version: 1.0.6 +ethosu_invoke NOP +ethosu_invoke NOP +ethosu_invoke NOP +ethosu_invoke COMMAND_STREAM +handle_command_stream: cmd_stream=0x61025be0, cms_length 1181 +QBASE=0x0000000061025be0, QSIZE=4724, base_pointer_offset=0x00000000 +BASEP0=0x0000000061026e60 +BASEP1=0x0000000060002f10 +BASEP2=0x0000000060002f10 +BASEP3=0x0000000061000fb0 +BASEP4=0x0000000060000fb0 +CMD=0x000Interrupt. status=0xffff0022, qread=4724 +CMD=0x00000006 +00006 +CMD=0x0000000c +ethosu_release_driver - Driver 0x20000de8 released +The image has been classified as 'tabby' +EXITTHESIM +Info: /OSCI/SystemC: Simulation stopped by user. +``` + +可以看到,输出的最后部分显示图像已被正确分类为「tabby」。 + +[下载 Python 源代码:micro_ethosu.py](https://tvm.apache.org/docs/_downloads/ab2eef18d10188532645b1d60fc7dd68/micro_ethosu.py) + +[下载 Jupyter Notebook:micro_ethosu.ipynb](https://tvm.apache.org/docs/_downloads/55a9eff88b1303e525d53269eeb16897/micro_ethosu.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/microtvm/04-microtvm_vm.md b/versioned_docs/version-0.12.0/how_to/microtvm/04-microtvm_vm.md new file mode 100644 index 00000000..36473959 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/microtvm/04-microtvm_vm.md @@ -0,0 +1,102 @@ +--- +title: microTVM 虚拟机参考手册 +--- + +# microTVM 虚拟机参考手册 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/work_with_microtvm/micro_reference_vm.html#sphx-glr-download-how-to-work-with-microtvm-micro-reference-vm-py) 下载完整的示例代码 +::: + +**作者**:[Andrew Reusch](mailto:areusch%40octoml.ai) + +本教程介绍如何参考虚拟机启动 microTVM,可以使用虚拟机在真实的物理硬件上进行开发,而无需单独安装 microTVM 依赖项,这种方法在使用 microTVM 重现行为时(如提交错误报告)也特别有用。 + +microTVM 使得 TVM 可以在裸机微控制器上构建和执行模型。 它旨在与各种 SoC 和 runtime 环境(即裸机、RTOS 等)兼容,microTVM 虚拟机参考手册可提供稳定环境来允许开发者共享和重现错误及结果。 + +## 工作原理 + +虚拟机没有存储到 TVM 仓库中——然而,`apps/microtvm/reference-vm` 中的文件描述了如何将虚拟机构建到 [Vagrant](https://vagrantup.com/) 虚拟机构建器工具。 + +VMs 参考手册分为两个部分: + +1. Vagrant Base Box 包含该平台的所有稳定依赖,构建脚本存储在 `apps/microtvm/reference-vm//base-box` 中,当平台的“稳定”依赖发生变化时,TVM committers 会运行这些,并且生成的基本 boxes 存储在 [Vagrant Cloud](https://app.vagrantup.com/tlcpack) 中。 +2. 通常用 Base Box 作为起点构建每个工作空间的虚拟机。构建脚本存储在 `apps/microtvm/reference-vm/` (除了 `base-box` 之外的所有内容)。 + +## 设置虚拟机 + +### 安装必要软件 + +确保最少安装以下软件: + +1. [Vagrant](https://vagrantup.com/) +2. 虚拟机监控器(**VirtualBox**、**Parallels** 或 **VMWare Fusion/Workstation**)。推荐使用 [VirtualBox](https://www.virtualbox.org/),它是一个免费的虚拟机监控器,注意,USB 转发需要安装 [VirtualBox 扩展包](https://www.virtualbox.org/wiki/Downloads#VirtualBox6.1.16OracleVMVirtualBoxExtensionPack) 。若使用 VirtualBox,还要安装 [vbguest](https://github.com/dotless-de/vagrant-vbguest) 插件。 +3. 若虚拟机监控器需要,可下载 [Vagrant 提供的插件](https://github.com/hashicorp/vagrant/wiki/Available-Vagrant-Plugins#providers)(或查看 [这里](https://www.vagrantup.com/vmware) 获取 VMWare 相关信息)。 + +### 首次启动 + +首次使用虚拟机参考手册时,要在本地创建 box,并对其进行配置。 + +``` bash +# 如果不使用 Zephyr,将 zephyr 替换为不同平台的名称。 +~/.../tvm $ cd apps/microtvm/reference-vm/zephyr +# 将 替换为要用的管理程序的名称(即 virtualbox、parallels、vmware_desktop)。 +~/.../tvm/apps/microtvm/reference-vm/zephyr $ vagrant up --provider= +``` + +此命令需要几分钟运行,并且需要4到5GB的存储空间,它执行的内容如下: + +1. 下载 [microTVM base box](https://app.vagrantup.com/tlcpack/boxes/microtvm) 并克隆它,形成特定于该 TVM 目录的新虚拟机。 +2. 把 TVM 目录(若使用 `git-subtree`,原始 `.git` 仓库)挂载到虚拟机中。 +3. 构建 TVM 并安装一个 Python virtualenv,其包含的依赖与 TVM 构建相对应。 + +### 将硬件连接到虚拟机 + +接下来配置 USB,将物理开发单板连接到虚拟机(而非直接连接到笔记本电脑的操作系统)。 + +推荐设置一个设备过滤器,而非一次性转发,因为编程时设备可能会重启,此时需要再次启用转发。这样做的好处是最终用户不会明显有感觉。参考教程: + +* [VirtualBox](https://www.virtualbox.org/manual/ch03.html#usb-support) +* [Parallels](https://kb.parallels.com/122993) +* [VMWare Workstation](https://docs.vmware.com/en/VMware-Workstation-Pro/15.0/com.vmware.ws.using.doc/GUID-E003456F-EB94-4B53-9082-293D9617CB5A.html) + +### 在虚拟机参考手册中重建 TVM + +首次启动后,确保在修改 C++ runtime 或 checkout 不同版本时,在 `$TVM_HOME/build-microtvm-zephyr` 中保持构建是最新的。可以重新配置机器(在运行 `vagrant up` 之前在同一目录中运行 `vagrant provision`)或自己手动重建 TVM。 + +注意:在虚拟机中构建的 TVM `.so` 可能与在主机上使用的不同,这就是它被构建在特殊目录 `build-microtvm-zephyr` 中的原因。 + +### 登录虚拟机 + +虚拟机应该仅对主机名为 `microtvm` 的主机可用,通过 SSH 连接到虚拟机: + +``` bash +$ vagrant ssh +``` + +然后 `cd` 到主机上用于 TVM 的相同路径,例如,在 Mac 上: + +``` bash +$ cd /Users/yourusername/path/to/tvm +``` + +## 运行测试 + +配置虚拟机后,可用 `poetry` 执行测试: + +``` bash +$ cd apps/microtvm/reference-vm/zephyr +$ poetry run python3 ../../../../tests/micro/zephyr/test_zephyr.py --zephyr-board=stm32f746g_disco +``` + +若没有连接物理硬件,但要用本地 QEMU 模拟器(在虚拟机中运行)运行测试,使用以下命令: + +``` bash +$ cd /Users/yourusername/path/to/tvm +$ cd apps/microtvm/reference-vm/zephyr/ +$ poetry run pytest ../../../../tests/micro/zephyr/test_zephyr.py --zephyr-board=qemu_x86 +``` + +[下载 Python 源代码:micro_reference_vm.py](https://tvm.apache.org/docs/_downloads/79027b28c061178b7ea56e3f047eeef1/micro_reference_vm.py) + +[下载 Jupyter notebook:micro_reference_vm.ipynb](https://tvm.apache.org/docs/_downloads/7ef06253b3d2676eb50e20a5f81ef8f9/micro_reference_vm.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/microtvm/05-microtvm_tflite.md b/versioned_docs/version-0.12.0/how_to/microtvm/05-microtvm_tflite.md new file mode 100644 index 00000000..e183c81f --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/microtvm/05-microtvm_tflite.md @@ -0,0 +1,331 @@ +--- +title: 支持 TFLite 模型的 microTVM +--- + +# 支持 TFLite 模型的 microTVM + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/work_with_microtvm/micro_tflite.html#sphx-glr-download-how-to-work-with-microtvm-micro-tflite-py) 下载完整的示例代码 +::: + +**作者**:[Tom Gall](https://github.com/tom-gall) + +本教程介绍如何用 microTVM 和支持 Relay 的 TFLite 模型。 + +:::note +若要在 microTVM 虚拟机参考手册上运行本教程,请根据页面底部的链接下载 Jupyter Notebook 并将其保存到 TVM 目录中。然后: + +1. 用修改后的 `vagrant ssh` 命令登录到虚拟机参考手册: + `$ vagrant ssh -- -L8888:localhost:8888` +2. 安装 Jupyter: `pip install jupyterlab` +3. `cd` 到 TVM 目录 +4. 安装 TFLite:`poetry install -E importer-tflite` +5. 启动 Jupyter Notebook:`jupyter notebook` +6. 复制 localhost URL,并将其粘贴到浏览器中 +7. 导航到已保存的 Jupyter Notebook(`.ipynb` 文件) +::: + +## 设置 + +### 安装 TFLite + +开始前,先安装 TFLite 包,两种安装方式如下所示: + +1. 使用 `pip` 安装 + + ``` bash + pip install tflite=2.1.0 --user + ``` + +2. 生成 TFLite 包,步骤如下: + + 获取 flatc 编译器,参阅 https://github.com/google/flatbuffers 了解详细信息,确保已正确安装。 + + ``` bash + flatc --version + ``` + + 获取 TFLite schema。 + + ``` bash + wget https://raw.githubusercontent.com/tensorflow/tensorflow/r1.13/tensorflow/lite/schema/schema.fbs + ``` + + 生成 TFLite 包。 + + ``` bash + flatc --python schema.fbs + ``` + + 将当前文件夹(包含生成的 TFLite 模块)添加到 PYTHONPATH。 + + ``` bash + export PYTHONPATH=${PYTHONPATH:+$PYTHONPATH:}$(pwd) + ``` + +用 `python -c "import tflite"` 验证 TFLite 包是否安装成功。 + +### 安装 Zephyr(仅限物理硬件) + +用主机模拟运行本教程时(默认),使用主机 `gcc` 构建模拟设备的固件镜像。若编译到物理硬件上运行,需安装一个 *toolchain* 以及一些特定于 target 的依赖。microTVM 允许用户提供任何可以启动 TVM RPC 服务器的编译器和 runtime。开始之前,请注意本教程依赖于 Zephyr RTOS 来提供这些部分。 + +参考 [安装说明](https://docs.zephyrproject.org/latest/getting_started/index.html) 安装 Zephyr。 + +**题外话:重新创建预训练 TFLite 模型** + + 本教程下载了预训练的 TFLite 模型。使用微控制器时,请注意这些设备的资源高度受限,像 MobileNet 这样的标准模型和小内存并不匹配。 + + 本教程使用 TF Micro 示例模型之一。 + + 若要复制训练步骤,参阅:https://github.com/tensorflow/tensorflow/tree/master/tensorflow/lite/micro/examples/hello_world/train + + :::note + 若不小心从 `wget https://storage.googleapis.com/download.tensorflow.org/models/tflite/micro/hello_world_2020_04_13.zip` 下载了示例预训练模型,会由于未实现的操作码(114)而失败。 + ::: + +## 加载并准备预训练模型 + +把预训练 TFLite 模型从当前目录中的文件中加载到 buffer + +``` python +import os +import json +import tarfile +import pathlib +import tempfile +import numpy as np + +import tvm +from tvm import relay +import tvm.contrib.utils +from tvm.contrib.download import download_testdata + +use_physical_hw = bool(os.getenv("TVM_MICRO_USE_HW")) +model_url = "https://people.linaro.org/~tom.gall/sine_model.tflite" +model_file = "sine_model.tflite" +model_path = download_testdata(model_url, model_file, module="data") + +tflite_model_buf = open(model_path, "rb").read() +``` + +利用 buffer,转换为 TFLite 模型 Python 对象 + +``` python +try: + import tflite + + tflite_model = tflite.Model.GetRootAsModel(tflite_model_buf, 0) +except AttributeError: + import tflite.Model + + tflite_model = tflite.Model.Model.GetRootAsModel(tflite_model_buf, 0) +``` + +打印模型的版本 + +``` python +version = tflite_model.Version() +print("Model Version: " + str(version)) +``` + +输出结果: + +``` bash +Model Version: 3 +``` + +解析 Python 模型对象,并转换为 Relay 模块和权重。注意输入张量的名称必须与模型中包含的内容相匹配。 + +若不确定,可通过 TensorFlow 项目中的 `visualize.py` 脚本来查看。参阅 [如何检查 .tflite 文件?](https://www.tensorflow.org/lite/guide/faq) + +``` python +input_tensor = "dense_4_input" +input_shape = (1,) +input_dtype = "float32" + +mod, params = relay.frontend.from_tflite( + tflite_model, shape_dict={input_tensor: input_shape}, dtype_dict={input_tensor: input_dtype} +) +``` + +## 定义 target + +接下来为 Relay 创建一个构建配置,关闭两个选项,然后调用 relay.build,为选定的 TARGET 生成一个 C 源文件。 + +当在与主机( Python 脚本执行的位置)相同架构的模拟 target 上运行时,为 TARGET 选择下面的「host」,选择 C Runtime 作为 RUNTIME ,并选择适当的单板/虚拟机来运行它(Zephyr 将创建基于 BOARD 的正确 QEMU 虚拟机)。 + +下面的示例中,选择 x86 架构并相应地选择 x86 虚拟机: + +``` python +RUNTIME = tvm.relay.backend.Runtime("crt", {"system-lib": True}) +TARGET = tvm.target.target.micro("host") + +# +# 为物理硬件编译 +# 在物理硬件上运行时,选择描述硬件的 TARGET 和 BOARD。 +# 下面的示例中选择 STM32F746 Nucleo target 和单板。另一种选择是 +# STM32F746 Discovery 板。由于该板具有与 Nucleo 相同的 MCU +# 板,但是一些接线和配置不同,选择「stm32f746g_disco」 +# 板生成正确的固件镜像。 +# + +if use_physical_hw: + boards_file = pathlib.Path(tvm.micro.get_microtvm_template_projects("zephyr")) / "boards.json" + with open(boards_file) as f: + boards = json.load(f) + + BOARD = os.getenv("TVM_MICRO_BOARD", default="nucleo_f746zg") + TARGET = tvm.target.target.micro(boards[BOARD]["model"]) + +# +# 对于某些单板,Zephyr 默认使用 QEMU 模拟运行,例如,下面 +# TARGET 和 BOARD 用于为 mps2-an521 开发板构建 microTVM 固件。自从那块板 +# 在 Zephyr 上默认模拟运行,板名称添加后缀「-qemu」通知 +# microTVM 必须使用 QEMU 传输器与开发板通信。如果名称 +# 已经有前缀「qemu_」,比如「qemu_x86」,就不需要加上那个后缀了。 +# +# TARGET = tvm.target.target.micro("mps2_an521") +# BOARD = "mps2_an521-qemu" +``` + +为 target 编译模型: + +``` python +with tvm.transform.PassContext( + opt_level=3, config={"tir.disable_vectorize": True}, disabled_pass=["AlterOpLayout"] +): + module = relay.build(mod, target=TARGET, runtime=RUNTIME, params=params) + +# 检查编译输出 +# --------------------------------- +# +# 编译过程产生了一些计算图中实现算子的 C 代码。 +# 可以通过打印 CSourceModule 内容来检查它(本教程 +# 只打印前 10 行): + +c_source_module = module.get_lib().imported_modules[0] +assert c_source_module.type_key == "c", "tutorial is broken" + +c_source_code = c_source_module.get_source() +first_few_lines = c_source_code.split("\n")[:10] +assert any( + l.startswith("TVM_DLL int32_t tvmgen_default_") for l in first_few_lines +), f"tutorial is broken: {first_few_lines!r}" +print("\n".join(first_few_lines)) + +# 编译生成的代码 +# ---------------------------- +# +# 下面需要将生成的 C 代码合并到一个项目中,以便在 +# 设备中运行推理。最简单的方法是自己集成,使用 microTVM 的标准输出格式 +# (:doc:`模型库格式` ``),这是具有标准布局的 tarball: + +# 获取可以存储 tarball 的临时路径(作为教程运行)。 + +fd, model_library_format_tar_path = tempfile.mkstemp() +os.close(fd) +os.unlink(model_library_format_tar_path) +tvm.micro.export_model_library_format(module, model_library_format_tar_path) + +with tarfile.open(model_library_format_tar_path, "r:*") as tar_f: + print("\n".join(f" - {m.name}" for m in tar_f.getmembers())) + +# 清理: +os.unlink(model_library_format_tar_path) + +# TVM 还为嵌入式平台提供了一个标准的方式来自动生成一个独立的 +# 项目,编译并烧录到一个 target,使用标准的 TVM RPC 与它通信。 +# 模型库格式用作此过程的模型输入。 +# 平台为嵌入时提供了集成,可以被 TVM 直接用于主机驱动 +# 推理和自动调优。这种集成由 +# `microTVM 项目 API` [https://github.com/apache/tvm-rfcs/blob/main/rfcs/0008-microtvm-project-api.md](https://github.com/apache/tvm-rfcs/blob/main/rfcs/0008-microtvm-project-api.md)_提供。 +# +# 嵌入式平台需要提供一个包含 microTVM API Server 的模板项目(通常, +# 存在于根目录中的“microtvm_api_server.py”文件中)。本教程使用示例“主机” +# 项目(使用 POSIX 子进程和管道模拟设备): + +template_project_path = pathlib.Path(tvm.micro.get_microtvm_template_projects("crt")) +project_options = {} # 可以使用 TVM 提供特定于平台 options。 + +# 编译物理硬件(或仿真板,如 mps_an521) +# -------------------------------------------------------------------------- +# 对于物理硬件,可以通过使用不同的模板项目来试用 Zephyr 平台 +# 和选项: +# + +if use_physical_hw: + template_project_path = pathlib.Path(tvm.micro.get_microtvm_template_projects("zephyr")) + project_options = {"project_type": "host_driven", "zephyr_board": BOARD} + +# 创建临时目录 +temp_dir = tvm.contrib.utils.tempdir() +generated_project_dir = temp_dir / "generated-project" +generated_project = tvm.micro.generate_project( + template_project_path, module, generated_project_dir, project_options +) + +# 构建并刷新项目 +generated_project.build() +generated_project.flash() +``` + +输出结果: + +``` c +// tvm target: c -keys=cpu -link-params=0 -model=host +#define TVM_EXPORTS +#include "tvm/runtime/c_runtime_api.h" +#include "tvm/runtime/c_backend_api.h" +#include +#ifdef __cplusplus +extern "C" +#endif +TVM_DLL int32_t tvmgen_default_fused_nn_dense_add(void* args, int32_t* arg_type_ids, int32_t num_args, void* out_ret_value, int32_t* out_ret_tcode, void* resource_handle) { + void* arg_placeholder = (((TVMValue*)args)[0].v_handle); + - . + - ./codegen + - ./codegen/host + - ./codegen/host/src + - ./codegen/host/src/default_lib0.c + - ./codegen/host/src/default_lib1.c + - ./codegen/host/src/default_lib2.c + - ./executor-config + - ./executor-config/graph + - ./executor-config/graph/default.graph + - ./metadata.json + - ./parameters + - ./parameters/default.params + - ./src + - ./src/default.relay +``` + +接下来,与模拟设备建立 session,并运行计算。*with session* 这一行通常会刷新连接的微控制器,但在本教程中,它只启动一个子进程来代替连接的微控制器。 + +``` python +with tvm.micro.Session(transport_context_manager=generated_project.transport()) as session: + graph_mod = tvm.micro.create_local_graph_executor( + module.get_graph_json(), session.get_system_lib(), session.device + ) + + # 使用「relay.build」产生的降级参数设置模型参数。 + graph_mod.set_input(**module.get_params()) + + # 模型使用单个 float32 值,并返回预测的正弦值。 + # 为传递输入值,我们构造一个带有单个构造值的 tvm.nd.array 对象作为输入。 + # 这个模型可接收的输入为 0 到 2Pi。 + graph_mod.set_input(input_tensor, tvm.nd.array(np.array([0.5], dtype="float32"))) + graph_mod.run() + + tvm_output = graph_mod.get_output(0).numpy() + print("result is: " + str(tvm_output)) +``` + +输出结果: + +``` bash +result is: [[0.4443792]] +``` + +[下载 Python 源代码:micro_tflite.py](https://tvm.apache.org/docs/_downloads/2fb9ae7bf124f72614a43137cf2919cb/micro_tflite.py) + +[下载 Jupyter Notebook:micro_tflite.ipynb](https://tvm.apache.org/docs/_downloads/5b279d8a8718816263fa65b0eef1a5c0/micro_tflite.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/microtvm/06-microtvm_arduino.md b/versioned_docs/version-0.12.0/how_to/microtvm/06-microtvm_arduino.md new file mode 100644 index 00000000..fa1bd39e --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/microtvm/06-microtvm_arduino.md @@ -0,0 +1,510 @@ +--- +title: 在 Arduino 上为 microTVM 训练视觉模型 +--- + +# 在 Arduino 上为 microTVM 训练视觉模型 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/work_with_microtvm/micro_train.html#sphx-glr-download-how-to-work-with-microtvm-micro-train-py) 下载完整的示例代码 +::: + +**作者**:[Gavin Uberti](https://github.com/guberti) + +本教程介绍如何训练 MobileNetV1 模型以适应嵌入式设备,以及如何使用 TVM 将这些模型部署到 Arduino。 + +:::note +推荐用 Jupyter Notebook 查看本教程的代码,可以用本页底部的链接下载并运行,或者使用 [Google Colab](https://colab.research.google.com/github/apache/tvm-site/blob/asf-site/docs/_downloads/a7c7ea4b5017ae70db1f51dd8e6dcd82/micro_train.ipynb) 免费在线查看。 +::: + +## 背景简介 + +构建物联网设备时,通常想让它们能够**看到并理解**它们周围的世界。可以采取多种形式,但通常设备也会想知道**某种物体**是否在其视野中。 + +例如,安全摄像头可能会寻找**人**,因此它可以决定是否将视频保存到内存中。红绿灯可能会寻找**汽车**,这样它就可以判断哪个信号灯应该首先改变。或者森林相机可能会寻找**一种动物**,从而估计动物种群的数量。 + +为使这些设备价格合理,我们希望为这些设备配置一个低成本处理器,如 [nRF52840](https://www.nordicsemi.com/Products/nRF52840)(在 Mouser 上每个售价 5 美元)或 [RP2040](https://www.raspberrypi.com/products/rp2040/)(每个只需 1.45 美元)。 + +这些设备的内存非常小(\~250 KB RAM),这意味着传统的边缘 AI 视觉模型(如 MobileNet 或 EfficientNet)都不能够运行。本教程将展示如何修改这些模型以解决此问题。然后,使用 TVM 为 Arduino 编译和部署。 + +## 安装必要软件 + +本教程使用 TensorFlow(Google 创建的一个广泛使用的机器学习库)来训练模型。TensorFlow 是一个非常底层的库,因此要用 Keras 接口来从 TensorFlow 获取信息。还会用 TensorFlow Lite 量化模型,因为 TensorFlow 本身不支持这一点。 + +生成模型后,使用 TVM 对其进行编译和测试。为避免必须从源代码构建,需要安装 `tlcpack` - TVM 的社区构建工具,还要安装 `imagemagick` 和 `curl` 来预处理数据: + +``` bash +%%bash +pip install -q tensorflow tflite +pip install -q tlcpack-nightly -f https://tlcpack.ai/wheels +apt-get -qq install imagemagick curl + +# 为 Nano 33 BLE 安装 Arduino CLI 和库 +curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh +/content/bin/arduino-cli core update-index +/content/bin/arduino-cli core install arduino:mbed_nano +``` + +### 使用 GPU + +本教程演示如何训练神经网络,训练神经网络需要大量的计算能力,使用 GPU 训练速度会更快。若在 Google Colab 上查看本教程,可通过 **Runtime->Change runtime type** 并选择“GPU”作为硬件加速器来启用 GPU。若在本地运行,则可按照 [TensorFlow 指南](https://www.tensorflow.org/guide/gpu) 进行操作。 + +使用以下代码测试 GPU 是否安装: + +``` python +import tensorflow as tf + +if not tf.test.gpu_device_name(): + print("No GPU was detected!") + print("Model training will take much longer (~30 minutes instead of ~5)") +else: + print("GPU detected - you're good to go.") +``` + +输出结果: + +``` bash +No GPU was detected! +Model training will take much longer (~30 minutes instead of ~5) +``` + +### 选择工作目录 + +选择工作目录,把图像数据集、训练好的模型和最终的 Arduino sketch 都放在此目录下,如果在 Google Colab 上运行,所有内容将保存在`/root`(又名 `~`)中,若在本地运行,可将其存储在其他地方。注意,此变量仅影响 Python 脚本 - 还必须调整 Bash 命令。 + +``` python +import os + +FOLDER = "/root" +``` + +## 下载数据 + +卷积神经网络通过大量图像以及标签来学习,为获得这些图像,需要一个公开可用的数据集,这个数据集中要包含数千张各种各样的目标的图像,以及每张图像中内容的标签,还需要一堆**不是**汽车的图像,因为我们需要区分这两个类别。 + +本教程将创建一个模型,来检测图像中是否包含**汽车**,也可以用于检测其他目标物体!只需将下面的源 URL 更改为包含另一种目标图像的源 URL。 + +下载 [斯坦福汽车数据集](https://hyper.ai/datasets/5466),该数据集包含 16,185 个彩色汽车图像。还需要不是汽车的随机物体的图像,这里我们使用的是 [COCO 2017](https://hyper.ai/datasets/4909) 验证集(比完整的训练集小,因此下载速度快,在完整数据集上进行训练效果更佳)。注意,COCO 2017 数据集中有一些汽车图像,但是数量很少,不会对结果产生影响 - 只会稍微降低准确度。 + +使用 TensorFlow 数据加载器程序,并改为手动操作,以确保能够轻松更改正在使用的数据集。最终得到以下文件层次结构: + +``` bash +/root +├── images +│ ├── object +│ │ ├── 000001.jpg +│ │ │ ... +│ │ └── 016185.jpg +│ ├── object.tgz +│ ├── random +│ │ ├── 000000000139.jpg +│ │ │ ... +│ │ └── 000000581781.jpg +│ └── random.zip +``` + +还应该注意到,斯坦福汽车有 8k 图像,而 COCO 2017 验证集是 5k 图像——并不是对半分割!若愿意可在训练期间对这些类进行不同的加权进行纠正,不纠正仍然可以进行有效训练。下载斯坦福汽车数据集大约需要 **2分钟**,而 COCO 2017 验证集需要 **1分钟**。 + +``` python +import os +import shutil +import urllib.request + +# 下载数据集 +os.makedirs(f"{FOLDER}/downloads") +os.makedirs(f"{FOLDER}/images") +urllib.request.urlretrieve( + "https://data.deepai.org/stanfordcars.zip", f"{FOLDER}/downloads/target.zip" +) +urllib.request.urlretrieve( + "http://images.cocodataset.org/zips/val2017.zip", f"{FOLDER}/downloads/random.zip" +) + +# 解压并重命名它们的文件夹 +shutil.unpack_archive(f"{FOLDER}/downloads/target.zip", f"{FOLDER}/downloads") +shutil.unpack_archive(f"{FOLDER}/downloads/random.zip", f"{FOLDER}/downloads") +shutil.move(f"{FOLDER}/downloads/cars_train/cars_train", f"{FOLDER}/images/target") +shutil.move(f"{FOLDER}/downloads/val2017", f"{FOLDER}/images/random") +``` + +输出结果: + +``` bash +'/tmp/tmpijas024t/images/random' +``` + +## 加载数据 + +目前,数据以各种大小的 JPG 文件形式存储在磁盘上。要使用其进行训练,必须将图像加载到内存中,并调整为 64x64,然后转换为原始的、未压缩的数据。可用 Keras 的 `image_dataset_from_directory` 来解决该问题,它加载图像时,每个像素值都是从 0 到 255 的浮点数。 + +从子目录结构中得知 `/objects` 中的图像是一类,而 `/random` 中的图像是另一类。设置 `label_mode='categorical'` 让 Keras 将这些转换为**分类标签**(一个 2x1 向量),对目标类的对象来说是 `[1, 0]`,对于其他任何东西来说是 `[0, 1]` 向量,此外,还将设置 `shuffle=True` 以随机化示例的顺序。 + +将样本分组以加快训练速度,设置 `batch_size = 32`。 + +最后,在机器学习中,通常希望输入是小数字。因此使用 `Rescaling` 层来更改图像,使每个像素都是 `0.0` 到 `1.0` 之间的浮点数,而不是 `0` 到 `255`。注意,因为要使用 `lambda` 函数 ,所以不要重新调整分类标签。 + +``` python +IMAGE_SIZE = (64, 64, 3) +unscaled_dataset = tf.keras.utils.image_dataset_from_directory( + f"{FOLDER}/images", + batch_size=32, + shuffle=True, + label_mode="categorical", + image_size=IMAGE_SIZE[0:2], +) +rescale = tf.keras.layers.Rescaling(scale=1.0 / 255) +full_dataset = unscaled_dataset.map(lambda im, lbl: (rescale(im), lbl)) +``` + +输出结果: + +``` bash +Found 13144 files belonging to 2 classes. +``` + +### 数据集中有什么? + +在将数据集提供给神经网络之前,应对其进行快速验证。数据是否能正确转换?标签是否合适?目标物体与其他物体的比例是多少?可以用 `matplotlib` 从数据集中显示一些示例: + +``` python +import matplotlib.pyplot as plt + +num_target_class = len(os.listdir(f"{FOLDER}/images/target/")) +num_random_class = len(os.listdir(f"{FOLDER}/images/random/")) +print(f"{FOLDER}/images/target contains {num_target_class} images") +print(f"{FOLDER}/images/random contains {num_random_class} images") + +# 显示一些样本及其标签 +SAMPLES_TO_SHOW = 10 +plt.figure(figsize=(20, 10)) +for i, (image, label) in enumerate(unscaled_dataset.unbatch()): + if i >= SAMPLES_TO_SHOW: + break + ax = plt.subplot(1, SAMPLES_TO_SHOW, i + 1) + plt.imshow(image.numpy().astype("uint8")) + plt.title(list(label.numpy())) + plt.axis("off") +``` + +![图片](https://tvm.apache.org/docs/_images/sphx_glr_micro_train_001.png) + +输出结果: + +``` bash +/tmp/tmpijas024t/images/target contains 8144 images +/tmp/tmpijas024t/images/random contains 5000 images +``` + +### 验证准确度 + +开发模型时经常要检查它的准确度(例如,看看它在训练期间是否有所改进)。如何做到这一点?可以在*所有*数据上训练模型,然后让它对相同的数据进行分类。但是,模型可以通过记住所有样本来作弊,这会使其*看起来*具有非常高的准确性,但实际上表现非常糟糕。在实践中,这种“记忆”被称为**过拟合**。 + +为防止这种情况,将留出一些数据(20%)作为**验证集**,用来检查模型的准确性。 + +``` python +num_batches = len(full_dataset) +train_dataset = full_dataset.take(int(num_batches * 0.8)) +validation_dataset = full_dataset.skip(len(train_dataset)) +``` + +## 加载数据 + +在过去的十年中,[卷积神经网络](https://en.wikipedia.org/wiki/Convolutional_neural_network) 已被广泛用于图像分类任务。[EfficientNet V2](https://arxiv.org/abs/2104.00298) 等最先进的模型的图像分类效果甚至优于人类。然而,这些模型有数千万个参数,它们无法运行在便宜的监控摄像头计算机上。 + +应用程序的准确度达到 90% 就足够了。因此可以使用更旧版本更小的 MobileNet V1 架构。但这*仍然*不够小,默认情况下,对于具有 224x224 输入和 alpha 1.0 的 MobileNet V1,仅**存储**就需要大约 50 MB。为了减小模型的大小,可调节三个 knob。 + +首先,可以将输入图像的大小从 224x224 减小到 96x96 或 64x64,Keras 可以轻松做到这一点。还可以将模型的 **alpha** 从 1.0 降低到 0.25,这使得网络的宽度(和过滤器的数量)缩小了四倍。如果真的空间有限,可以通过让模型拍摄灰度图像而不是 RGB 图像来减少**通道**数量。 + +在本教程中,使用 RGB 64x64 输入图像和 alpha 0.25。效果虽然不是很理想,但它允许完成的模型适合 192 KB 的 RAM,同时仍然允许使用官方 TensorFlow 源模型执行迁移学习(如果使用 alpha \<0.25 或灰度输入,无法做到这样)。 + +### 什么是迁移学习? + +深度学习长期以来一直 [主导图像分类](https://paperswithcode.com/sota/image-classification-on-imagenet),但训练神经网络需要大量时间。当一个神经网络“从头开始”训练时,起初参数会随机初始化,缓慢地学习如何区分图像。 + +从一个**已经**擅长特定任务的神经网络开始迁移学习,在此示例中,该任务是对 [ImageNet 数据集](https://www.image-net.org/) 中的图像进行分类。这意味着神经网络已经具有一些目标检测。 + +这对于像 MobileNet 这样的图像处理神经网络特别有效,在实践中,模型的卷积层(即前 90% 的层)用于识别 line 和 shape 等低级特征——只有最后几个全连接层用于确定,这些 shape 如何构成网络要检测的目标。 + +可以通过使用在 ImageNet 上训练的 MobileNet 模型开始训练,该模型已经知道如何识别线条和形状。从这个预训练模型中删除最后几层,并添加自己的最终层。在汽车与非汽车数据集上训练这个联合模型,以调优第一层并从头开始训练最后一层。这种训练已经部分训练过的模型称为*微调*。 + +源 MobileNets 模型(用于迁移学习) [已被 TensorFlow 人员预训练](https://github.com/tensorflow/models/blob/master/research/slim/nets/mobilenet_v1.md),因此可以下载最接近于我们想要的版本( 128x128 输入模型,深度为 0.25)。 + +``` python +os.makedirs(f"{FOLDER}/models") +WEIGHTS_PATH = f"{FOLDER}/models/mobilenet_2_5_128_tf.h5" +urllib.request.urlretrieve( + "https://storage.googleapis.com/tensorflow/keras-applications/mobilenet/mobilenet_2_5_128_tf.h5", + WEIGHTS_PATH, +) + +pretrained = tf.keras.applications.MobileNet( + input_shape=IMAGE_SIZE, weights=WEIGHTS_PATH, alpha=0.25 +) +``` + +### 修改网络 + +如上所述,预训练模型旨在对 1,000 个 ImageNet 类别进行分类,但我们希望将其转换为对汽车进行分类。由于只有最后几层是特定于任务的,因此**切断原始模型的最后五层**,通过执行 respape、dropout、flatten 和 softmax 操作来构建自己的模型的最后几层。 + +```plain +model = tf.keras.models.Sequential() + +model.add(tf.keras.layers.InputLayer(input_shape=IMAGE_SIZE)) +model.add(tf.keras.Model(inputs=pretrained.inputs, outputs=pretrained.layers[-5].output)) + +model.add(tf.keras.layers.Reshape((-1,))) +model.add(tf.keras.layers.Dropout(0.1)) +model.add(tf.keras.layers.Flatten()) +model.add(tf.keras.layers.Dense(2, activation="softmax")) +``` + +### 微调网络 + +在训练神经网络时,必须设置**学习率**来控制网络学习速度。太慢和太快都会导致学习效果不好。通常对于 Adam(正在使用的优化器)来说,`0.001` 是一个相当不错的学习率(并且是 [原始论文](https://arxiv.org/abs/1412.6980) 中推荐的)。在本示例中,`0.0005` 效果更好。 + +将之前的验证集传递给 `model.fit`,在每次训练时评估模型性能,并能够跟踪模型性能是如何提升的。训练完成后,模型的验证准确率应该在 `0.98` 左右(这意味着在验证集上训练 100 次,其中 98 次都是预测正确的)。 + +``` python +model.compile( + optimizer=tf.keras.optimizers.Adam(learning_rate=0.0005), + loss="categorical_crossentropy", + metrics=["accuracy"], +) +model.fit(train_dataset, validation_data=validation_dataset, epochs=3, verbose=2) +``` + +输出结果: + +``` bash +Epoch 1/3 +328/328 - 56s - loss: 0.2151 - accuracy: 0.9280 - val_loss: 0.1213 - val_accuracy: 0.9615 +Epoch 2/3 +328/328 - 53s - loss: 0.1029 - accuracy: 0.9623 - val_loss: 0.1111 - val_accuracy: 0.9626 +Epoch 3/3 +328/328 - 52s - loss: 0.0685 - accuracy: 0.9750 - val_loss: 0.1541 - val_accuracy: 0.9505 + + +``` + +## 量化 + +通过改变输入维度,以及移除底层,将模型减少到只有 219k 个参数,每个参数都是 `float32` 类型,占用 4 个字节,因此模型将占用将近 1 MB! + +此外,硬件可能没有内置对浮点数的支持,虽然大多数高内存 Arduino(如 Nano 33 BLE)确实有硬件支持,但其他一些(如 Arduino Due)则没有。在任何*没有*专用硬件支持的板上,浮点乘法都会非常慢。 + +为解决这两个问题可以将模型**量化**,把权重表示为八位整数,为获得最佳性能,TensorFlow 会跟踪模型中每个神经元的激活方式,因此可以得出,如何最准确地用整数运算,来模拟神经元的原始激活。 + +可以创建一个具有代表性的数据集来帮助 TensorFlow 实现——原始数据集的一个子集,用于跟踪这些神经元的激活方式。然后将其传递给带有 `Optimize` 标志的 `TFLiteConverter` (Keras 本身不支持量化),以告诉 TFLite 执行转换。默认情况下,TFLite 将模型的输入和输出保持为浮点数,因此必须明确告诉它避免这种行为。 + +``` python +def representative_dataset(): + for image_batch, label_batch in full_dataset.take(10): + yield [image_batch] + +converter = tf.lite.TFLiteConverter.from_keras_model(model) +converter.optimizations = [tf.lite.Optimize.DEFAULT] +converter.representative_dataset = representative_dataset +converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] +converter.inference_input_type = tf.uint8 +converter.inference_output_type = tf.uint8 + +quantized_model = converter.convert() +``` + +### 下载所需模型 + +现已经完成了一个模型,可以在本地或其他教程中使用它(尝试自动调优此模型或在 https://netron.app/ 上查看)。但在做这些事情之前,必须将它写入一个文件(`quantized.tflite`)。如果你在 Google Colab 上运行本教程,需要取消注释最后两行才能在编写文件后下载文件。 + +``` python +QUANTIZED_MODEL_PATH = f"{FOLDER}/models/quantized.tflite" +with open(QUANTIZED_MODEL_PATH, "wb") as f: + f.write(quantized_model) +# from google.colab import files +# files.download(QUANTIZED_MODEL_PATH) +``` + +## 使用 TVM 为 Arduino 编译 + +TensorFlow 有一个用于部署到微控制器的内置框架——[TFLite Micro](https://www.tensorflow.org/lite/microcontrollers)。但是,开发板不能很好地支持它,并且不支持自动调优,因此改用 Apache TVM。 + +TVM 可以与其命令行界面(`tvmc`)或 Python 界面一起使用。Python 接口功能齐全,且更稳定,因此这里用 Python。 + +TVM 是一个优化编译器,对模型的优化是通过**中间表示**分阶段执行的。其中第一个是 [Relay](https://arxiv.org/abs/1810.00952)(一种强调可移植性的高级 intermediate representation)。从 `.tflite` 到 Relay 的转换是在不知道“最终目标”的情况下完成的,我们打算在 Arduino 上运行这个模型。 + +### 选择 Arduino 板 + +接下来确定要使用哪个 Arduino 板。最终生成的 Arduino sketch 应该与任何板子兼容,但是提前知道使用的是哪个板子,可以让 TVM 调整其编译策略从而获得更好的性能。 + +有一点需要注意:要有足够的**内存**(闪存和 RAM)来运行我们的模型,在 Arduino Uno 上无法运行像 MobileNet 这样的复杂的视觉模型,该板只有 2 kB 的 RAM 和 32 kB 的闪存!而模型有大约 200,000 个参数,因此无法拟合。 + +本教程使用具有 1 MB 闪存和 256 KB RAM 的 Nano 33 BLE,其他具有更高规格的 Arduino 同样适用。 + +### 生成项目 + +接下来把模型编译为 TVM 的 MLF(模型库格式)intermediate representation,它由 C/C++ 代码组成,专为自动调优而设计。为了提高性能,我们将告诉 TVM 我们正在为 `nrf52840` 微处理器(Nano 33 BLE 使用的那个)进行编译。此外,还告诉 TVM 使用 C runtime(缩写为 `crt`),并使用 ahead-of-time 内存分配(缩写为 `aot`,有助于减少模型的内存占用)。最后,由于 C 没有原生向量化类型,所以用 `"tir.disable_vectorize": True` 禁用向量化,。 + +设置了这些配置参数后,调用 `tvm.relay.build` 将 Relay 模型编译为 MLF intermediate representation。此后,只需要调用 `tvm.micro.generate_project` 并传入 Arduino 模板项目即可完成编译。 + +``` python +import shutil +import tflite +import tvm + +# 在 TFLite 1 和 2 中加载模型的方法不同 +try: # TFLite 2.1 and above # TFLite 2.1 及以上 + tflite_model = tflite.Model.GetRootAsModel(quantized_model, 0) +except AttributeError: # Fall back to TFLite 1.14 method # 回退到 TFLite 1.14 方法 + tflite_model = tflite.Model.Model.GetRootAsModel(quantized_model, 0) + +# 转换为 Relay 中间表示 +mod, params = tvm.relay.frontend.from_tflite(tflite_model) + +# 设置配置标志以提高性能 +target = tvm.target.target.micro("nrf52840") +runtime = tvm.relay.backend.Runtime("crt") +executor = tvm.relay.backend.Executor("aot", {"unpacked-api": True}) + +# 转换为 MLF 中间表示 +with tvm.transform.PassContext(opt_level=3, config={"tir.disable_vectorize": True}): + mod = tvm.relay.build(mod, target, runtime=runtime, executor=executor, params=params) + +# 从 MLF 中间表示生成一个 Arduino 项目 +shutil.rmtree(f"{FOLDER}/models/project", ignore_errors=True) +arduino_project = tvm.micro.generate_project( + tvm.micro.get_microtvm_template_projects("arduino"), + mod, + f"{FOLDER}/models/project", + { + "arduino_board": "nano33ble", + "arduino_cli_cmd": "/content/bin/arduino-cli", + "project_type": "example_project", + }, +) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +## 测试 Arduino 项目 + +加载下面两张 224x224 图像(一张是汽车,一张不是汽车),然后执行编译模型来测试 Arduino 项目。 + +![图片](/img/docs/tlc-pack/web-data/main/testdata/microTVM/data/model_train_images_combined.png) + +这些是可以从 Imgur 下载的 224x224 PNG 图像。在输入这些图像之前,需要调整它们的大小并转换为原始数据,这可以使用 `imagemagick` 完成。 + +由于只编译 C/CPP 文件(和类似文件),因此将原始数据加载到 Arduino 上具有一定的挑战性。可以通过使用内置程序 `bin2c` 将原始数据嵌入硬编码 C 数组中来解决此问题,该程序输出如下: + +``` c +static const unsigned char CAR_IMAGE[] = { + 0x22,0x23,0x14,0x22, + ... + 0x07,0x0e,0x08,0x08 +}; +``` + +可以用几行 Bash 代码来完成这两件事: + +``` bash +%%bash +mkdir -p ~/tests +curl "https://i.imgur.com/JBbEhxN.png" -o ~/tests/car_224.png +convert ~/tests/car_224.png -resize 64 ~/tests/car_64.png +stream ~/tests/car_64.png ~/tests/car.raw +bin2c -c -st ~/tests/car.raw --name CAR_IMAGE ~/models/project/car.c + +curl "https://i.imgur.com/wkh7Dx2.png" -o ~/tests/catan_224.png +convert ~/tests/catan_224.png -resize 64 ~/tests/catan_64.png +stream ~/tests/catan_64.png ~/tests/catan.raw +bin2c -c -st ~/tests/catan.raw --name CATAN_IMAGE ~/models/project/catan.c +``` + +## 编写 Arduino 脚本 + +现需要 Arduino 代码来读取刚刚生成的两个二进制数组,并运行模型,把输出记录到串行监视器。该文件将替换 `arduino_sketch.ino` 作为 sketch 的主文件。 + +``` c +%%writefile /root/models/project.ino +#include "src/model.h" +#include "car.c" +#include "catan.c" + +void setup() { + Serial.begin(9600); + TVMInitialize(); +} + +void loop() { + uint8_t result_data[2]; + Serial.println("Car results:"); + TVMExecute(const_cast(CAR_IMAGE), result_data); + Serial.print(result_data[0]); Serial.print(", "); + Serial.print(result_data[1]); Serial.println(); + + Serial.println("Other object results:"); + TVMExecute(const_cast(CATAN_IMAGE), result_data); + Serial.print(result_data[0]); Serial.print(", "); + Serial.print(result_data[1]); Serial.println(); + + delay(1000); +} +``` + +### 编译代码 + +项目已经生成,TVM 的工作就基本完成了!仍然可以调用 `arduino_project.build()` 和 `arduino_project.upload()`,它们只是使用了 `arduino-cli` 的编译和 flash 命令。另外一个教程中会介绍如何自动调优我们的模型。最后,验证项目没有引发编译器错误: + +``` python +shutil.rmtree(f"{FOLDER}/models/project/build", ignore_errors=True) +arduino_project.build() +print("Compilation succeeded!") +``` + +输出结果: + +``` bash +Compilation succeeded! +``` + +## 上传到设备 + +最后一步是将 sketch 上传到 Arduino 以确保代码正常工作。Google Colab 不能做到这一点,所以必须下载 sketch。只需将项目转换为 *.zip* 文件,然后调用 *files.download*。若你在 Google Colab 上运行,则需取消最后两行注释才能在编写文件后下载文件。 + +``` python +ZIP_FOLDER = f"{FOLDER}/models/project" +shutil.make_archive(ZIP_FOLDER, "zip", ZIP_FOLDER) +# from google.colab import files +# files.download(f"{FOLDER}/models/project.zip") +``` + +在 Arduino IDE 中打开 sketch,必须为你所使用的板下载 IDE 和 SDK。对于某些板卡,例如 Sony SPRESENSE,可能需要更改设置以控制板卡使用多少内存。 + +### 预期结果 + +若一切正常,应该在串行监视器上看到以下输出: + +``` bash +Car results: +255, 0 +Other object results: +0, 255 +``` + +第一个数字代表模型判断物体**是**汽车的置信度,范围为 0-255。第二个数字代表模型判断物体**不是**汽车的置信度,范围也是 0-255。以上结果表示模型非常确定第一张图像是汽车,而第二张图像不是汽车。这个判断是正确的,表明模型正常运行了。 + +## 总结 + +本教程使用迁移学习来快速训练图像识别模型,并用来识别汽车。修改模型的输入尺寸和神经网络的最后几层,使模型效果更好,同时更快更小。然后量化模型并使用 TVM 进行编译创建 Arduino sketch。最后,使用两个静态图像对模型进行测试,证明它可以按预期工作。 + +### 下一步 + +修改模型从摄像头读取实时图像,[在 GitHub](https://github.com/guberti/tvm-arduino-demos/tree/master/examples/person_detection) 上有另一个 Arduino 教程说明如何操作。也可以 [使用 TVM 的自动调整功能](https://tvm.apache.org/docs/how_to/work_with_microtvm/micro_autotune.html) 来显著提高模型的性能。 + +**脚本总运行时长:**( 4 分 44.392 秒) + +[下载 Python 源代码:micro_train.py](https://tvm.apache.org/docs/_downloads/b52cec46baf4f78d6bcd94cbe269c8a6/micro_train.py) + +[下载 Jupyter Notebook:micro_train.ipynb](https://tvm.apache.org/docs/_downloads/a7c7ea4b5017ae70db1f51dd8e6dcd82/micro_train.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/microtvm/07-tvmc_micro.md b/versioned_docs/version-0.12.0/how_to/microtvm/07-tvmc_micro.md new file mode 100644 index 00000000..8f566859 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/microtvm/07-tvmc_micro.md @@ -0,0 +1,143 @@ +--- +title: 使用 TVMC Micro 执行微模型 +--- + +# 使用 TVMC Micro 执行微模型 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/work_with_microtvm/micro_tvmc.html#sphx-glr-download-how-to-work-with-microtvm-micro-tvmc-py) 下载完整的示例代码 +::: + +**作者**:[Mehrdad Hessar](https://github.com/mehrdadh) + +本教程介绍了如何为微型设备编译一个微模型,并在 Zephyr 平台上构建一个程序,来执行这个模型,烧录程序,并用 tvmc micro 命令来执行所有模型。 + +:::note +本教程将介绍如何在 Zephyr 平台上使用 TVMC Mirco。学习本教程前,请安装 Zephyr 依赖项,或通过以下方式(已经安装了 Zephyr 依赖)之一运行本教程。 + +* 使用 [microTVM 虚拟机参考手册](https://tvm.apache.org/docs/how_to/work_with_microtvm/micro_reference_vm.html#sphx-glr-how-to-work-with-microtvm-micro-reference-vm-py)。 +* 使用 TVM 提供的 QEMU Docker 镜像。下载并登录到 Docker 镜像: + +``` bash +cd tvm +./docker/bash.sh tlcpack/ci-qemu +``` +::: + +## 使用 TVMC Micro + +TVMC 是一个命令行工具,也是 TVM Python 包的一部分。机器设置不同,访问此软件包的方式也不一样。多数情况下,可以直接使用 `tvmc` 命令。如果在 `$PYTHONPATH` 上将 TVM 作为 Python 模块,你可以使用 `python -m tvm.driver.tvmc` 命令来访问此驱动程序。简单起见,本教程使用 `tvmc` 命令。 + +检查是否安装了 TVMC 命令,运行如下命令: + +``` bash +tvmc --help +``` + +使用 `tvmc compile` 子命令为 microtvm 编译模型,此命令的输出在后续步骤中与 `tvmc micro` 子命令一起使用。使用以下命令检查 TVMC Micro 是否可用: + +``` bash +tvmc micro --help +``` + +使用 `tvmc micro` 执行的主要任务是 `create`、`build` 和 `flash`。要了解各个子命令下的特定选项,可使用 `tvmc micro --help`。本教程会使用每个子命令。 + +## 获取微模型 + +本教程使用 TFLite micro 的 Magic Wand 模型(一种深度卷积层模型,可通过加速度传感器识别手势)。 + +本教程使用 TFLite 格式的模型。 + +``` bash +wget https://github.com/tensorflow/tflite-micro/raw/main/tensorflow/lite/micro/examples/magic_wand/magic_wand.tflite +``` + +## 将 TFLite 模型编译为模型库格式 + +模型库格式(Model Library Format,简称 MLF)是 TVM 为微 target 提供的一种输出格式,MLF 是包含了 TVM 编译器输出的所有部分的 tarball,这些编译器输出可以用于 TVM 环境之外的微 target。更多信息请访问 [模型库格式](https://tvm.apache.org/docs/arch/model_library_format.html)。 + +为 `qemu_x86` Zephyr 板生成一个 MLF 文件,为 `magic_wand` TFLite 模型生成 MLF 输出: + +``` bash +tvmc compile magic_wand.tflite \ + --target='c -keys=cpu -link-params=0 -model=host' \ + --runtime=crt \ + --runtime-crt-system-lib 1 \ + --executor='graph' \ + --executor-graph-link-params 0 \ + --output model.tar \ + --output-format mlf \ + --pass-config tir.disable_vectorize=1 \ + --disabled-pass=AlterOpLayout +``` + +这将生成一个包含 TVM 编译器输出文件的 `model.tar` 文件。若要为不同的 Zephyr 设备运行此命令,需要更新 `target`。例如,对于 `nrf5340dk_nrf5340_cpuapp` 板,target 是 `--target='c -keys=cpu -link-params=0 -model=nrf5340dk'`。 + +## 使用模型库格式创建 Zephyr 项目 + +使用 TVM Micro 子命令 `create` 生成 Zephyr 项目。将 MLF 格式、项目路径,以及项目选项传递给 `create` 子命令。每个平台(Zephyr/Arduino)的项目选项在其项目 API 服务器文件中定义。运行如下命令生成 Zephyr 项目: + +``` bash +tvmc micro create \ + project \ + model.tar \ + zephyr \ + --project-option project_type=host_driven zephyr_board=qemu_x86 +``` + +以上命令为 `qemu_x86` Zephyr 板生成一个 `Host-Driven` Zephyr 项目,在 Host-Driven 模板项目中,图执行器(Graph Executor)将在主机上运行,并通过使用 RPC 机制向设备发出命令,在 Zephyr 设备上运行模型执行。阅读有关[主机驱动执行](https://tvm.apache.org/docs/arch/microtvm_design.html#host-driven-execution)的更多信息。 + +获取有关 TVMC Micro `create` 子命令的更多信息,执行如下命令: + +``` bash +tvmc micro create --help +``` + +## 使用 TVMC Micro 构建和烧录 Zephyr 项目 + +接下来使用如下命令构建 Zephyr 项目(包括用于运行微模型的 TVM 生成代码、用于在主机驱动模式下运行模型的 Zephyr 模板代码和 TVM runtime 源/头文件)。要构建项目: + +``` bash +tvmc micro build \ + project \ + zephyr \ + --project-option zephyr_board=qemu_x86 +``` + +以上命令将在 `project` 目录中构建项目,并在 `project/build` 下生成二进制文件,要为不同的 Zephyr 板构建 Zephyr 项目,需更改 `zephyr_board` 项目选项。 + +接下来把 Zephyr 二进制文件烧录到 Zephyr 设备。对于 `qemu_x86` Zephyr 板,因为要用到 QEMU,所以不会执行任何操作,但是对于物理硬件,此步骤不可省略。 + +``` bash +tvmc micro flash \ + project \ + zephyr \ + --project-option zephyr_board=qemu_x86 +``` + +## 在微 Target 上运行微模型 + +与设备通信后,在设备上对编译好的模型和 TVM RPC 服务器进行编程。Zephyr 板等待主机打开通信通道。MicroTVM 设备通常使用串口通信(UART)进行通信 。要使用 TVMC 在设备上运行闪存模型,可通过 `tvmc run` 子命令并通过 `--device micro` 来指定设备类型,打开通信通道,使用主机上的 `Graph Executor` 设置输入值并在设备上运行完整模型,然后从设备获取输出。 + +``` bash +tvmc run \ + --device micro \ + project \ + --project-option zephyr_board=qemu_x86 \ + --fill-mode ones \ + --print-top 4 + + # Output: + # + # INFO:__main__:b'[100%] [QEMU] CPU: qemu32,+nx,+pae\n' + # remote: microTVM Zephyr runtime - running + # INFO:__main__:b'[100%] Built target run\n' + # [[3. 1. 2. 0. ] + # [0.47213247 0.41364592 0.07525456 0.03896701]] +``` + +具体来说,此命令将模型的输入全部设置为 1,并显示输出的四个值及其索引。 + +[下载 Python 源代码:micro_tvmc.py](https://tvm.apache.org/docs/_downloads/eb483c672b88006c331115968e0ffd9b/micro_tvmc.py) + +[下载 Jupyter notebook:micro_tvmc.ipynb](https://tvm.apache.org/docs/_downloads/6e511f5a8ddbf12f2fca2dfadc0cc4a9/micro_tvmc.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/microtvm/_category_.json b/versioned_docs/version-0.12.0/how_to/microtvm/_category_.json new file mode 100644 index 00000000..fa4c219d --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/microtvm/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 180 +} diff --git a/versioned_docs/version-0.12.0/how_to/microtvm/index.md b/versioned_docs/version-0.12.0/how_to/microtvm/index.md new file mode 100644 index 00000000..534129a2 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/microtvm/index.md @@ -0,0 +1,13 @@ +--- +title: 使用 microTVM +--- + +microTVM 支持在裸机平台(例如,那些没有像 Linux、OS X 或 Windows 这样的传统操作系统的平台)上进行推理。以下演示了如何用 microTVM 调优和部署模型。 + +* [microTVM 主机驱动的 AoT](aot) +* [使用 microTVM 自动调优](autotune_microtvm) +* [在带有 CMSIS-NN 的 Arm(R) Cortex(R)-M55 CPU 和 Ethos(TM)-U55 NPU 裸机上运行 TVM](tvm_arm) +* [microTVM 虚拟机参考手册](microtvm_vm) +* [带有 TFLite 模型的 microTVM](microtvm_tflite) +* [在 Arduino 上训练 microTVM 的视觉模型](microtvm_arduino) +* [使用 TVMC Micro 执行微型模型](tvmc_micro) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/models/01-PAPI.md b/versioned_docs/version-0.12.0/how_to/models/01-PAPI.md new file mode 100644 index 00000000..5621df8b --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/models/01-PAPI.md @@ -0,0 +1,83 @@ +--- +title: PAPI 入门 +--- + +# PAPI 入门 + +性能应用程序编程接口(Performance Application Programming Interface,简称 PAPI)是一个可在各种平台上提供性能计数器的库。在指定的运行期间,性能计数器提供处理器行为的准确底层信息,包含简单的指标,如总循环计数、缓存未命中和执行的指令,以及更高级的信息(如总 FLOPS 和 warp 占用率)。PAPI 的这些指标在 profiling 时可用。 + +## 安装 PAPI + +PAPI 可以用包管理器(Ubuntu 上用 `apt-get install libpapi-dev` 命令)来安装,也可以从 [源代码](https://bitbucket.org/icl/papi/src/master/) 安装。 + +由于之前从源代码 pull 最新版本的 PAPI 导致了构建问题,因此推荐 checkout 标记版本 `papi-6-0-0-1-t`。 + +## 使用 PAPI 构建 TVM + +若要在 TVM 构建中包含 PAPI,在 `config.cmake` 中设置: + +``` cmake +set(USE_PAPI ON) +``` + +若 PAPI 安装在非标准位置,可指定它的位置: + +``` cmake +set(USE_PAPI path/to/papi.pc) +``` + +## 在 Profiling 时使用 PAPI + +若 TVM 是用 PAPI 构建的(见上文),可将 `tvm.runtime.profiling.PAPIMetricCollector` 传给 `tvm.runtime.GraphModule.profile()` 来收集性能指标: + +``` python +target = "llvm" +dev = tvm.cpu() +mod, params = mlp.get_workload(1) + +exe = relay.vm.compile(mod, target, params=params) +vm = profiler_vm.VirtualMachineProfiler(exe, dev) + +data = tvm.nd.array(np.random.rand(1, 1, 28, 28).astype("float32"), device=dev) +report = vm.profile( + [data], + func_name="main", + collectors=[tvm.runtime.profiling.PAPIMetricCollector()], +) +print(report) +``` +``` bash +Name perf::CACHE-MISSES perf::CYCLES perf::STALLED-CYCLES-BACKEND perf::INSTRUCTIONS perf::STALLED-CYCLES-FRONTEND +fused_nn_dense_nn_bias_add_nn_relu 2,494 1,570,698 85,608 675,564 39,583 +fused_nn_dense_nn_bias_add_nn_relu_1 1,149 655,101 13,278 202,297 21,380 +fused_nn_dense_nn_bias_add 288 600,184 8,321 163,446 19,513 +fused_nn_batch_flatten 301 587,049 4,636 158,636 18,565 +fused_nn_softmax 154 575,143 8,018 160,738 18,995 +---------- +Sum 4,386 3,988,175 119,861 1,360,681 118,036 +Total 10,644 8,327,360 179,310 2,660,569 270,044 +``` + +还可以指定收集哪些指标: + +``` python +report = vm.profile( + [data], + func_name="main", + collectors=[tvm.runtime.profiling.PAPIMetricCollector({dev: ["PAPI_FP_OPS"])], +) +``` + +``` bash +Name PAPI_FP_OPS +fused_nn_dense_nn_bias_add_nn_relu 200,832 +fused_nn_dense_nn_bias_add_nn_relu_1 16,448 +fused_nn_dense_nn_bias_add 1,548 +fused_nn_softmax 160 +fused_nn_batch_flatten 0 +---------- +Sum 218,988 +Total 218,988 +``` + +运行 `papi_avail` 和 `papi_native_avail` 命令可得到可用指标列表。 diff --git a/versioned_docs/version-0.12.0/how_to/models/_category_.json b/versioned_docs/version-0.12.0/how_to/models/_category_.json new file mode 100644 index 00000000..70a2b90c --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/models/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 200 +} diff --git a/versioned_docs/version-0.12.0/how_to/models/index.md b/versioned_docs/version-0.12.0/how_to/models/index.md new file mode 100644 index 00000000..6e43afd0 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/models/index.md @@ -0,0 +1,6 @@ +--- +title: Profile 模型 +--- + +# Profile 模型 +* [PAPI 入门](PAPI) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/optimize/01-cpu_conv.md b/versioned_docs/version-0.12.0/how_to/optimize/01-cpu_conv.md new file mode 100644 index 00000000..2c01c2ce --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/optimize/01-cpu_conv.md @@ -0,0 +1,634 @@ +--- +title: 如何在 CPU 上优化 GEMM +--- + +# 如何在 CPU 上优化 GEMM + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/optimize_operators/opt_gemm.html#sphx-glr-download-how-to-optimize-operators-opt-gemm-py) 下载完整的示例代码 +::: + +**作者**:[Jian Weng](https://github.com/were), [Ruofei Yu](https://github.com/yuruofeifei) + +TVM 提供抽象接口,允许用户分别描述算法和算法的实现(所谓的调度)。通常,以高性能调度编写算法会破坏算法的可读性和模块化。此外,尝试各种看似有希望的 schedules 非常耗时。在 TVM 的帮助下,可以有效地尝试这些 schedules 以提高性能。 + +本教程将演示如何用 TVM 优化矩阵乘法,并通过 18 行代码实现比 baseline 快 200 倍的性能。 + +在 CPU 上执行的密集计算应用程序有两个重要的优化: + +1. 提高内存访问的 cache 命中率。高 cache 命中率可以加速复杂的数值计算和热点内存访问。需要将原始内存访问模式转换为适合 cache 策略的模式。 +2. SIMD(单指令多数据),或者称之为向量处理单元,每次都会处理一小批数据,而不是单个网格。需要将循环体中的数据访问模式转换为统一模式,以便 LLVM 后端可以将其降级到 SIMD。 + +实际上,本教程中使用的所有方法都在这个 [repo](https://github.com/flame/how-to-optimize-gemm) 中提到了。其中一些已被 TVM 抽象自动应用,但有一些由于 TVM 的限制,不能被简单地应用。 + +下面提到的所有实验结果,都是在配备 Intel i7-4770HQ CPU 的 2015 年 15 英寸 MacBook 上执行的,所有 x86 CPU 的高速缓存行大小应为 64 字节。 + +## 准备和 baseline + +本教程演示如何使用 TVM 优化矩阵乘法。实际演示前,首先定义这些变量。然后编写一个 baseline 实现,这是在 TVM 中编写矩阵乘法的最简单方法。 + +``` python +import tvm +import tvm.testing +from tvm import te +import numpy +import timeit + +# 矩阵的大小 +# (M, K) x (K, N) +# 可自由尝试不同的 shapes,有时 TVM 优化在 MKL 中的表现优于 numpy。 +M = 1024 +K = 1024 +N = 1024 + +# tvm 中的默认张量类型 +dtype = "float32" + +# 为 SIMD 使用英特尔 AVX2(高级向量扩展)ISA +# 要获得最佳性能,更改以下行 +# 为 llvm -mcpu=core-avx2,或者使用的特定类型的 CPU +target = "llvm" +dev = tvm.device(target, 0) + +# 用于测试的随机生成张量 +a = tvm.nd.array(numpy.random.rand(M, K).astype(dtype), dev) +b = tvm.nd.array(numpy.random.rand(K, N).astype(dtype), dev) + +np_repeat = 100 +np_runing_time = timeit.timeit( + setup="import numpy\n" + "M = " + str(M) + "\n" + "K = " + str(K) + "\n" + "N = " + str(N) + "\n" + 'dtype = "float32"\n' + "a = numpy.random.rand(M, K).astype(dtype)\n" + "b = numpy.random.rand(K, N).astype(dtype)\n", + stmt="answer = numpy.dot(a, b)", + number=np_repeat, +) +print("Numpy running time: %f" % (np_runing_time / np_repeat)) + +answer = numpy.dot(a.numpy(), b.numpy()) + +# 算法 +k = te.reduce_axis((0, K), "k") +A = te.placeholder((M, K), name="A") +B = te.placeholder((K, N), name="B") +C = te.compute((M, N), lambda m, n: te.sum(A[m, k] * B[k, n], axis=k), name="C") + +# 默认 schedule +s = te.create_schedule(C.op) +func = tvm.build(s, [A, B, C], target=target, name="mmult") +assert func + +c = tvm.nd.array(numpy.zeros((M, N), dtype=dtype), dev) +func(a, b, c) +tvm.testing.assert_allclose(c.numpy(), answer, rtol=1e-5) + +evaluator = func.time_evaluator(func.entry_name, dev, number=1) +print("Baseline: %f" % evaluator(a, b, c).mean) +``` + +输出结果: + +``` bash +Numpy running time: 0.018437 +Baseline: 3.336375 +``` + +在 TVM 中,始终可以检查较低级别的 IR 以调试或优化 schedule。这是使用 baseline schedule 生成的 IR。 + +``` python +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []), + B: Buffer(B_2: Pointer(float32), float32, [1048576], []), + C: Buffer(C_2: Pointer(float32), float32, [1048576], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [1024, 1024], []), B_1: B_3: Buffer(B_2, float32, [1024, 1024], []), C_1: C_3: Buffer(C_2, float32, [1024, 1024], [])} { + for (m: int32, 0, 1024) { + for (n: int32, 0, 1024) { + C[((m*1024) + n)] = 0f32 + for (k: int32, 0, 1024) { + let cse_var_2: int32 = (m*1024) + let cse_var_1: int32 = (cse_var_2 + n) + C[cse_var_1] = (C[cse_var_1] + (A[(cse_var_2 + k)]*B[((k*1024) + n)])) + } + } + } +} +``` + +## 分块 + +提高缓存命中率的一个重要技巧是分块——数据块将逐块计算。块内的内存访问是一个局部性的小邻域。本教程选择 32 作为分块因子,因此该块将填充 32 * 32 * sizeof(float) ,即总大小为 32KB 的缓存中的 4KB(L1 数据缓存)。 + +``` python +bn = 32 +kfactor = 4 +s = te.create_schedule(C.op) + +# 通过循环 tiling 进行分块 +mo, no, mi, ni = s[C].tile(C.op.axis[0], C.op.axis[1], bn, bn) +(kaxis,) = s[C].op.reduce_axis +ko, ki = s[C].split(kaxis, factor=kfactor) + +# 将 reduction 域提升到分块循环之外 +s[C].reorder(mo, no, ko, ki, mi, ni) + +func = tvm.build(s, [A, B, C], target=target, name="mmult") +assert func + +c = tvm.nd.array(numpy.zeros((M, N), dtype=dtype), dev) +func(a, b, c) +tvm.testing.assert_allclose(c.numpy(), answer, rtol=1e-5) + +# 通过简单地将循环 32x32 分块,并将 ko、ki 提升到分块循环之外, +# 可以看到与 baseline 相比,加速有很大提升。 +evaluator = func.time_evaluator(func.entry_name, dev, number=10) +print("Opt1: %f" % evaluator(a, b, c).mean) +``` + +输出结果: + +``` bash +Opt1: 0.307321 +``` + +分块后生成的 IR: + +``` bash +print(tvm.lower(s, [A, B, C], simple_mode=True)) +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []), + B: Buffer(B_2: Pointer(float32), float32, [1048576], []), + C: Buffer(C_2: Pointer(float32), float32, [1048576], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [1024, 1024], []), B_1: B_3: Buffer(B_2, float32, [1024, 1024], []), C_1: C_3: Buffer(C_2, float32, [1024, 1024], [])} { + for (m.outer: int32, 0, 32) { + for (n.outer: int32, 0, 32) { + for (m.inner.init: int32, 0, 32) { + for (n.inner.init: int32, 0, 32) { + C[((((m.outer*32768) + (m.inner.init*1024)) + (n.outer*32)) + n.inner.init)] = 0f32 + } + } + for (k.outer: int32, 0, 256) { + for (k.inner: int32, 0, 4) { + for (m.inner: int32, 0, 32) { + for (n.inner: int32, 0, 32) { + let cse_var_3: int32 = (n.outer*32) + let cse_var_2: int32 = ((m.outer*32768) + (m.inner*1024)) + let cse_var_1: int32 = ((cse_var_2 + cse_var_3) + n.inner) + C[cse_var_1] = (C[cse_var_1] + (A[((cse_var_2 + (k.outer*4)) + k.inner)]*B[((((k.outer*4096) + (k.inner*1024)) + cse_var_3) + n.inner)])) + } + } + } + } + } + } +} +``` + +## 向量化 + +另一个重要技巧是向量化,当内存访问模式一致时,编译器可以检测到这种模式并将连续内存传递给向量处理器。TVM 中可以用 vectorize 接口来提示编译器这种模式,这样就可以进行加速。 + +本教程选择向量化内部循环 row data(对缓存更友好)。 + +``` python +s = te.create_schedule(C.op) +mo, no, mi, ni = s[C].tile(C.op.axis[0], C.op.axis[1], bn, bn) +(kaxis,) = s[C].op.reduce_axis +ko, ki = s[C].split(kaxis, factor=kfactor) + +s[C].reorder(mo, no, ko, ki, mi, ni) + +# 向量化 +s[C].vectorize(ni) + +func = tvm.build(s, [A, B, C], target=target, name="mmult") +assert func + +c = tvm.nd.array(numpy.zeros((M, N), dtype=dtype), dev) +func(a, b, c) +tvm.testing.assert_allclose(c.numpy(), answer, rtol=1e-5) + +evaluator = func.time_evaluator(func.entry_name, dev, number=10) +print("Opt2: %f" % evaluator(a, b, c).mean) +``` + +输出结果: + +``` bash +Opt2: 0.349439 +``` + +向量化后生成的 IR: + +``` python +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []), + B: Buffer(B_2: Pointer(float32), float32, [1048576], []), + C: Buffer(C_2: Pointer(float32), float32, [1048576], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [1024, 1024], []), B_1: B_3: Buffer(B_2, float32, [1024, 1024], []), C_1: C_3: Buffer(C_2, float32, [1024, 1024], [])} { + for (m.outer: int32, 0, 32) { + for (n.outer: int32, 0, 32) { + for (m.inner.init: int32, 0, 32) { + C[ramp((((m.outer*32768) + (m.inner.init*1024)) + (n.outer*32)), 1, 32)] = broadcast(0f32, 32) + } + for (k.outer: int32, 0, 256) { + for (k.inner: int32, 0, 4) { + for (m.inner: int32, 0, 32) { + let cse_var_3: int32 = (n.outer*32) + let cse_var_2: int32 = ((m.outer*32768) + (m.inner*1024)) + let cse_var_1: int32 = (cse_var_2 + cse_var_3) + C[ramp(cse_var_1, 1, 32)] = (C[ramp(cse_var_1, 1, 32)] + (broadcast(A[((cse_var_2 + (k.outer*4)) + k.inner)], 32)*B[ramp((((k.outer*4096) + (k.inner*1024)) + cse_var_3), 1, 32)])) + } + } + } + } + } +} +``` + +## 循环置换 + +查看上面的 IR,可以看到内部循环的 row data 对于 B 和 C 都是向量化的。接下来查看 A 的访问模式。在当前调度中,A 是逐列访问的,但它对缓存不友好。如果改变 ki 和内轴 mi 的嵌套循环顺序,A 矩阵的访问模式对缓存更友好。 + +``` python +s = te.create_schedule(C.op) +mo, no, mi, ni = s[C].tile(C.op.axis[0], C.op.axis[1], bn, bn) +(kaxis,) = s[C].op.reduce_axis +ko, ki = s[C].split(kaxis, factor=kfactor) + +# 重新排序 +s[C].reorder(mo, no, ko, mi, ki, ni) +s[C].vectorize(ni) + +func = tvm.build(s, [A, B, C], target=target, name="mmult") +assert func + +c = tvm.nd.array(numpy.zeros((M, N), dtype=dtype), dev) +func(a, b, c) +tvm.testing.assert_allclose(c.numpy(), answer, rtol=1e-5) + +evaluator = func.time_evaluator(func.entry_name, dev, number=10) +print("Opt3: %f" % evaluator(a, b, c).mean) +``` + +输出结果: + +``` bash +Opt3: 0.115375 +``` + +循环置换后生成的 IR: + +``` python +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []), + B: Buffer(B_2: Pointer(float32), float32, [1048576], []), + C: Buffer(C_2: Pointer(float32), float32, [1048576], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [1024, 1024], []), B_1: B_3: Buffer(B_2, float32, [1024, 1024], []), C_1: C_3: Buffer(C_2, float32, [1024, 1024], [])} { + for (m.outer: int32, 0, 32) { + for (n.outer: int32, 0, 32) { + for (m.inner.init: int32, 0, 32) { + C[ramp((((m.outer*32768) + (m.inner.init*1024)) + (n.outer*32)), 1, 32)] = broadcast(0f32, 32) + } + for (k.outer: int32, 0, 256) { + for (m.inner: int32, 0, 32) { + for (k.inner: int32, 0, 4) { + let cse_var_3: int32 = (n.outer*32) + let cse_var_2: int32 = ((m.outer*32768) + (m.inner*1024)) + let cse_var_1: int32 = (cse_var_2 + cse_var_3) + C[ramp(cse_var_1, 1, 32)] = (C[ramp(cse_var_1, 1, 32)] + (broadcast(A[((cse_var_2 + (k.outer*4)) + k.inner)], 32)*B[ramp((((k.outer*4096) + (k.inner*1024)) + cse_var_3), 1, 32)])) + } + } + } + } + } +} +``` + +## 数组打包 + +另一个重要的技巧是数组打包,对多维数组的存储进行重新排序,展平并存储在一维内存中,方便顺序访问。 + +![图片](https://github.com/dmlc/web-data/raw/main/tvm/tutorial/array-packing.png) + +注意:此图是数组打包工作原理的一般说明。 + +可以用数组打包来解决 B 的访问模式。观察展平后 B 的数组访问模式,当迭代 K 维时,它不是顺序的。可以用维度 [K][N] 对 B 重新排序,使其具有 [N/bn][K][bn] 维度,其中 bn 是分块因子,也是内循环中 B 的向量大小。 + +这种重新排序将 N 拆分为两个维度——bigN(N/bn)和 littleN(bn)——新维度 [N/bn][K][bn] 匹配 B 从外部到内部循环的索引(no, ko, ki, ni) 在展平后导致 B 的顺序访问模式。 + +``` python +# 我们必须稍微重新编写算法。 +packedB = te.compute( + (N / bn, K, bn), lambda bigN, k, littleN: B[k, bigN * bn + littleN], name="packedB" +) +C = te.compute( + (M, N), + lambda m, n: te.sum(A[m, k] * packedB[n // bn, k, tvm.tir.indexmod(n, bn)], axis=k), + name="C", +) + +s = te.create_schedule(C.op) + +mo, no, mi, ni = s[C].tile(C.op.axis[0], C.op.axis[1], bn, bn) +(kaxis,) = s[C].op.reduce_axis +ko, ki = s[C].split(kaxis, factor=kfactor) + +s[C].reorder(mo, no, ko, mi, ki, ni) +s[C].vectorize(ni) + +bigN, _, littleN = s[packedB].op.axis +s[packedB].vectorize(littleN) +s[packedB].parallel(bigN) + +func = tvm.build(s, [A, B, C], target=target, name="mmult") +assert func + +c = tvm.nd.array(numpy.zeros((M, N), dtype=dtype), dev) +func(a, b, c) +tvm.testing.assert_allclose(c.numpy(), answer, rtol=1e-5) + +evaluator = func.time_evaluator(func.entry_name, dev, number=10) +print("Opt4: %f" % evaluator(a, b, c).mean) +``` + +输出结果: + +``` bash +Opt4: 0.109499 +``` + +数组打包后生成的 IR: + +``` python +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []), + B: Buffer(B_2: Pointer(float32), float32, [1048576], []), + C: Buffer(C_2: Pointer(float32), float32, [1048576], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [1024, 1024], []), B_1: B_3: Buffer(B_2, float32, [1024, 1024], []), C_1: C_3: Buffer(C_2, float32, [1024, 1024], [])} { + allocate(packedB: Pointer(global float32x32), float32x32, [32768]), storage_scope = global { + for (bigN: int32, 0, 32) "parallel" { + for (k: int32, 0, 1024) { + packedB_1: Buffer(packedB, float32x32, [32768], [])[((bigN*1024) + k)] = B[ramp(((k*1024) + (bigN*32)), 1, 32)] + } + } + for (m.outer: int32, 0, 32) { + for (n.outer: int32, 0, 32) { + for (m.inner.init: int32, 0, 32) { + C[ramp((((m.outer*32768) + (m.inner.init*1024)) + (n.outer*32)), 1, 32)] = broadcast(0f32, 32) + } + for (k.outer: int32, 0, 256) { + for (m.inner: int32, 0, 32) { + for (k.inner: int32, 0, 4) { + let cse_var_3: int32 = ((m.outer*32768) + (m.inner*1024)) + let cse_var_2: int32 = (k.outer*4) + let cse_var_1: int32 = (cse_var_3 + (n.outer*32)) + C[ramp(cse_var_1, 1, 32)] = (C[ramp(cse_var_1, 1, 32)] + (broadcast(A[((cse_var_3 + cse_var_2) + k.inner)], 32)*packedB_1[(((n.outer*1024) + cse_var_2) + k.inner)])) + } + } + } + } + } + } +} +``` + +## 块的写缓存 + +分块后,程序会逐块将结果写入 C(访问模式不是顺序的),因此,可以使用顺序缓存数组来保存块结果,并在所有块结果准备好时写入 C。 + +``` python +s = te.create_schedule(C.op) + +# 分配写缓存 +CC = s.cache_write(C, "global") +mo, no, mi, ni = s[C].tile(C.op.axis[0], C.op.axis[1], bn, bn) + +# 写缓存在 no 被计算 +s[CC].compute_at(s[C], no) + +# 新的内轴 +mc, nc = s[CC].op.axis + +(kaxis,) = s[CC].op.reduce_axis +ko, ki = s[CC].split(kaxis, factor=kfactor) +s[CC].reorder(ko, mc, ki, nc) +s[CC].vectorize(nc) + +# TODO: 添加单独的优化步骤,来讨论循环展开 +# unrolling 是一种循环优化策略,可以减少分支 +# 预测失败,以及增加并发执行的机会 +# 展开 kfactor 循环 +s[CC].unroll(ki) + +bigN, _, littleN = s[packedB].op.axis +s[packedB].vectorize(littleN) +s[packedB].parallel(bigN) + +func = tvm.build(s, [A, B, C], target=target, name="mmult") +assert func + +c = tvm.nd.array(numpy.zeros((M, N), dtype=dtype), dev) +func(a, b, c) +tvm.testing.assert_allclose(c.numpy(), answer, rtol=1e-5) + +evaluator = func.time_evaluator(func.entry_name, dev, number=10) +print("Opt5: %f" % evaluator(a, b, c).mean) +``` + +输出结果: + +``` bash +Opt5: 0.110823 +``` + +分块后生成的 IR: + +``` bash +print(tvm.lower(s, [A, B, C], simple_mode=True)) +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []), + B: Buffer(B_2: Pointer(float32), float32, [1048576], []), + C: Buffer(C_2: Pointer(float32), float32, [1048576], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [1024, 1024], []), B_1: B_3: Buffer(B_2, float32, [1024, 1024], []), C_1: C_3: Buffer(C_2, float32, [1024, 1024], [])} { + allocate(packedB: Pointer(global float32x32), float32x32, [32768]), storage_scope = global; + allocate(C.global: Pointer(global float32), float32, [1024]), storage_scope = global { + for (bigN: int32, 0, 32) "parallel" { + for (k: int32, 0, 1024) { + packedB_1: Buffer(packedB, float32x32, [32768], [])[((bigN*1024) + k)] = B[ramp(((k*1024) + (bigN*32)), 1, 32)] + } + } + for (m.outer: int32, 0, 32) { + for (n.outer: int32, 0, 32) { + for (m.c.init: int32, 0, 32) { + C.global_1: Buffer(C.global, float32, [1024], [])[ramp((m.c.init*32), 1, 32)] = broadcast(0f32, 32) + } + for (k.outer: int32, 0, 256) { + for (m.c: int32, 0, 32) { + let cse_var_4: int32 = (k.outer*4) + let cse_var_3: int32 = (m.c*32) + let cse_var_2: int32 = ((n.outer*1024) + cse_var_4) + let cse_var_1: int32 = (((m.outer*32768) + (m.c*1024)) + cse_var_4) + { + C.global_1[ramp(cse_var_3, 1, 32)] = (C.global_1[ramp(cse_var_3, 1, 32)] + (broadcast(A[cse_var_1], 32)*packedB_1[cse_var_2])) + C.global_1[ramp(cse_var_3, 1, 32)] = (C.global_1[ramp(cse_var_3, 1, 32)] + (broadcast(A[(cse_var_1 + 1)], 32)*packedB_1[(cse_var_2 + 1)])) + C.global_1[ramp(cse_var_3, 1, 32)] = (C.global_1[ramp(cse_var_3, 1, 32)] + (broadcast(A[(cse_var_1 + 2)], 32)*packedB_1[(cse_var_2 + 2)])) + C.global_1[ramp(cse_var_3, 1, 32)] = (C.global_1[ramp(cse_var_3, 1, 32)] + (broadcast(A[(cse_var_1 + 3)], 32)*packedB_1[(cse_var_2 + 3)])) + } + } + } + for (m.inner: int32, 0, 32) { + for (n.inner: int32, 0, 32) { + C[((((m.outer*32768) + (m.inner*1024)) + (n.outer*32)) + n.inner)] = C.global_1[((m.inner*32) + n.inner)] + } + } + } + } + } +} +``` + +## 并行化 + +此外,还可以利用多核处理器进行线程级并行化。 + +``` python +s = te.create_schedule(C.op) + +CC = s.cache_write(C, "global") + +mo, no, mi, ni = s[C].tile(C.op.axis[0], C.op.axis[1], bn, bn) + +s[CC].compute_at(s[C], no) + +mc, nc = s[CC].op.axis + +(kaxis,) = s[CC].op.reduce_axis +ko, ki = s[CC].split(kaxis, factor=kfactor) +s[CC].reorder(ko, mc, ki, nc) +s[CC].vectorize(nc) +s[CC].unroll(ki) + +# 并行 +s[C].parallel(mo) + +bigN, _, littleN = s[packedB].op.axis +s[packedB].vectorize(littleN) +s[packedB].parallel(bigN) + +func = tvm.build(s, [A, B, C], target=target, name="mmult") +assert func + +c = tvm.nd.array(numpy.zeros((M, N), dtype=dtype), dev) +func(a, b, c) +tvm.testing.assert_allclose(c.numpy(), answer, rtol=1e-5) + +evaluator = func.time_evaluator(func.entry_name, dev, number=50) +opt6_time = evaluator(a, b, c).mean +print("Opt6: %f" % opt6_time) +``` + +输出结果: + +``` bash +Opt6: 0.144875 +``` + +并行化后生成的 IR: + +``` python +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []), + B: Buffer(B_2: Pointer(float32), float32, [1048576], []), + C: Buffer(C_2: Pointer(float32), float32, [1048576], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [1024, 1024], []), B_1: B_3: Buffer(B_2, float32, [1024, 1024], []), C_1: C_3: Buffer(C_2, float32, [1024, 1024], [])} { + allocate(packedB: Pointer(global float32x32), float32x32, [32768]), storage_scope = global { + for (bigN: int32, 0, 32) "parallel" { + for (k: int32, 0, 1024) { + packedB_1: Buffer(packedB, float32x32, [32768], [])[((bigN*1024) + k)] = B[ramp(((k*1024) + (bigN*32)), 1, 32)] + } + } + for (m.outer: int32, 0, 32) "parallel" { + allocate(C.global: Pointer(global float32), float32, [1024]), storage_scope = global; + for (n.outer: int32, 0, 32) { + for (m.c.init: int32, 0, 32) { + C.global_1: Buffer(C.global, float32, [1024], [])[ramp((m.c.init*32), 1, 32)] = broadcast(0f32, 32) + } + for (k.outer: int32, 0, 256) { + for (m.c: int32, 0, 32) { + let cse_var_4: int32 = (k.outer*4) + let cse_var_3: int32 = (m.c*32) + let cse_var_2: int32 = ((n.outer*1024) + cse_var_4) + let cse_var_1: int32 = (((m.outer*32768) + (m.c*1024)) + cse_var_4) + { + C.global_1[ramp(cse_var_3, 1, 32)] = (C.global_1[ramp(cse_var_3, 1, 32)] + (broadcast(A[cse_var_1], 32)*packedB_1[cse_var_2])) + C.global_1[ramp(cse_var_3, 1, 32)] = (C.global_1[ramp(cse_var_3, 1, 32)] + (broadcast(A[(cse_var_1 + 1)], 32)*packedB_1[(cse_var_2 + 1)])) + C.global_1[ramp(cse_var_3, 1, 32)] = (C.global_1[ramp(cse_var_3, 1, 32)] + (broadcast(A[(cse_var_1 + 2)], 32)*packedB_1[(cse_var_2 + 2)])) + C.global_1[ramp(cse_var_3, 1, 32)] = (C.global_1[ramp(cse_var_3, 1, 32)] + (broadcast(A[(cse_var_1 + 3)], 32)*packedB_1[(cse_var_2 + 3)])) + } + } + } + for (m.inner: int32, 0, 32) { + for (n.inner: int32, 0, 32) { + C[((((m.outer*32768) + (m.inner*1024)) + (n.outer*32)) + n.inner)] = C.global_1[((m.inner*32) + n.inner)] + } + } + } + } + } +} +``` + +## 总结 + +应用上述简单优化后,仅用 18 行代码,就可以达到使用 MKL *numpy* 性能的 60%。注意,网页上的输出反映了非专有 Docker 容器上的运行时间,是*不可靠*的。推荐自己运行本教程,观察 TVM 的性能提升。 + +[下载 Python 源代码:opt_gemm.py](https://tvm.apache.org/docs/_downloads/96137df89d8034b548f407123ec50ce9/opt_gemm.py) + +[下载 Jupyter Notebook:opt_gemm.ipynb](https://tvm.apache.org/docs/_downloads/0f8d36b3ffd04a5a08089dc671eb788e/opt_gemm.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/optimize/02-gpu_conv.md b/versioned_docs/version-0.12.0/how_to/optimize/02-gpu_conv.md new file mode 100644 index 00000000..2becf6e1 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/optimize/02-gpu_conv.md @@ -0,0 +1,198 @@ +--- +title: 如何在 GPU 上优化卷积 +--- + +# 如何在 GPU 上优化卷积 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/optimize_operators/opt_conv_cuda.html#sphx-glr-download-how-to-optimize-operators-opt-conv-cuda-py) 下载完整的示例代码 +::: + +**作者**:[Haichen Shen](https://homes.cs.washington.edu/\~haichen/) + +本教程演示了如何在 TVM 中编写高性能卷积实现。以正方形大小的输入张量和滤波器为例,假设卷积输入的 batch 较大。在此示例中,使用不同的布局来存储数据,以实现更好的数据局部性。缓冲区布局是 HWCN,分别代表高度、宽度、通道、batch。 + +## 准备和算法 + +对具有 256 个通道和 14 x 14 维度的输入张量使用固定大小。batch size 为 256,卷积过滤器包含 512 个大小为 3 x 3 的过滤器,用步长为 1 和 padding size 为 1 进行卷积。以下代码定义了 TVM 中的卷积算法。 + +``` python +import numpy as np +import tvm +from tvm import te + +# 输入和过滤器的大小 +batch = 256 +in_channel = 256 +out_channel = 512 +in_size = 14 +kernel = 3 +pad = 1 +stride = 1 + +# 算法 +A = te.placeholder((in_size, in_size, in_channel, batch), name="A") +W = te.placeholder((kernel, kernel, in_channel, out_channel), name="W") +out_size = (in_size - kernel + 2 * pad) // stride + 1 +# Pad 输入 +Apad = te.compute( + (in_size + 2 * pad, in_size + 2 * pad, in_channel, batch), + lambda yy, xx, cc, nn: tvm.tir.if_then_else( + tvm.tir.all(yy >= pad, yy - pad < in_size, xx >= pad, xx - pad < in_size), + A[yy - pad, xx - pad, cc, nn], + tvm.tir.const(0.0, "float32"), + ), + name="Apad", +) +# 创建归约变量 +rc = te.reduce_axis((0, in_channel), name="rc") +ry = te.reduce_axis((0, kernel), name="ry") +rx = te.reduce_axis((0, kernel), name="rx") +# 计算卷积 +B = te.compute( + (out_size, out_size, out_channel, batch), + lambda yy, xx, ff, nn: te.sum( + Apad[yy * stride + ry, xx * stride + rx, rc, nn] * W[ry, rx, rc, ff], axis=[ry, rx, rc] + ), + name="B", +) +``` + +## 内存层次结构 + +首先指定缓冲区的内存层次结构。下图显示了 GPU 内存层次结构,与 CPU 内存层次结构的重要区别是 GPU 提供了一个称为共享内存的缓存缓冲区,由程序员管理。因此,如何最大化共享内存中的数据重用对于在 GPU 内核中实现高性能至关重要。 + +![图片](https://github.com/dmlc/web-data/raw/main/tvm/tutorial/gpu_memory_hierarchy.png) + +在本例中,将 Apad 和 W 加载到缓冲区 AA 和 WW 中(存储在共享内存中)。这些缓冲区稍后将由同一线程块中的所有线程共享以计算卷积,然后每个线程将自己的部分从共享缓冲区加载到它们的本地寄存器 AL 和 WL 中。BL 是输出 B 的本地缓存,也存储在线程本地寄存器中。 + +``` python +# 指定内存层次结构 +s = te.create_schedule(B.op) +s[Apad].compute_inline() # compute Apad inline +AA = s.cache_read(Apad, "shared", [B]) +WW = s.cache_read(W, "shared", [B]) +AL = s.cache_read(AA, "local", [B]) +WL = s.cache_read(WW, "local", [B]) +BL = s.cache_write(B, "local") +``` + +## 分块 + +以下代码将工作负载拆分为线程块和单独的线程,遵循矩阵乘法中的分块方案。如下图所示,给定一个像素坐标(y、x),一个线程块负责计算一个 block_factor x block_factor (64 x 64) 的区域,用于输出通道和 batch。由于共享内存空间的限制,每次只从 Apad 和 B 加载 step x block_factor (8 x 64) 数据到共享内存中的缓冲区。 + +![图片](https://github.com/dmlc/web-data/raw/main/tvm/tutorial/conv_gpu_blocking.png) + +``` python +# 平铺常量 +tile = 8 +num_thread = 8 +block_factor = tile * num_thread +step = 8 +vthread = 2 + +# 获取 GPU 线程索引 +block_x = te.thread_axis("blockIdx.x") +block_y = te.thread_axis("blockIdx.y") +block_z = te.thread_axis("blockIdx.z") +thread_x = te.thread_axis((0, num_thread), "threadIdx.x") +thread_y = te.thread_axis((0, num_thread), "threadIdx.y") +thread_xz = te.thread_axis((0, vthread), "vthread", name="vx") +thread_yz = te.thread_axis((0, vthread), "vthread", name="vy") + +# split 工作负载 +hi, wi, fi, ni = s[B].op.axis +bz = s[B].fuse(hi, wi) +by, fi = s[B].split(fi, factor=block_factor) +bx, ni = s[B].split(ni, factor=block_factor) + +# 将迭代变量绑定到 GPU 线程索引 +s[B].bind(bz, block_z) +s[B].bind(by, block_y) +s[B].bind(bx, block_x) +``` + +## 虚拟线程分割 + +进一步将工作负载从线程块拆分为单个线程。为了避免 *memory bank conflict*,使用虚拟线程将区域分成 4 个部分,然后平铺成 8x8 的网格。因此,如下图所示,每个线程计算 4 个跨步网格,每个网格的大小为 4 x 4。 + +![图片](https://github.com/dmlc/web-data/raw/main/tvm/tutorial/conv_gpu_vthread.png) + +``` python +tyz, fi = s[B].split(fi, nparts=vthread) # 虚拟线程 split +txz, ni = s[B].split(ni, nparts=vthread) # 虚拟线程 split +ty, fi = s[B].split(fi, nparts=num_thread) +tx, ni = s[B].split(ni, nparts=num_thread) +s[B].reorder(bz, by, bx, tyz, txz, ty, tx, fi, ni) + +s[B].bind(tyz, thread_yz) +s[B].bind(txz, thread_xz) +s[B].bind(ty, thread_y) +s[B].bind(tx, thread_x) +``` + +## 协同获取(Cooperative Fetching) + +如前所述,每个时间步长都要将 step x block_factor 数据从 GPU 全局内存传输到共享内存。为了减少每个线程的内存传输,以下代码让同一线程块中的线程协同从全局内存中获取相关数据。 + +``` python +# Schedule BL 本地写入 +s[BL].compute_at(s[B], tx) +yi, xi, fi, ni = s[BL].op.axis +ry, rx, rc = s[BL].op.reduce_axis +rco, rci = s[BL].split(rc, factor=step) +s[BL].reorder(rco, ry, rx, rci, fi, ni) + +# 将计算附加到迭代变量 +s[AA].compute_at(s[BL], rx) +s[WW].compute_at(s[BL], rx) +s[AL].compute_at(s[BL], rci) +s[WL].compute_at(s[BL], rci) + +# A 的共享内存负载调度 +yi, xi, ci, ni = s[AA].op.axis +ty, ci = s[AA].split(ci, nparts=num_thread) +tx, ni = s[AA].split(ni, nparts=num_thread) +_, ni = s[AA].split(ni, factor=4) +s[AA].reorder(ty, tx, yi, xi, ci, ni) +s[AA].bind(ty, thread_y) +s[AA].bind(tx, thread_x) +s[AA].vectorize(ni) # 向量化内存加载 + +# W 的共享内存负载调度 +yi, xi, ci, fi = s[WW].op.axis +ty, ci = s[WW].split(ci, nparts=num_thread) +tx, fi = s[WW].split(fi, nparts=num_thread) +_, fi = s[WW].split(fi, factor=4) +s[WW].reorder(ty, tx, yi, xi, ci, fi) +s[WW].bind(ty, thread_y) +s[WW].bind(tx, thread_x) +s[WW].vectorize(fi) # 向量化内存加载 +``` + +## 生成 CUDA 内核 + +最后用 TVM 生成和编译 CUDA 内核,并评估卷积的延迟。 + +``` python +func = tvm.build(s, [A, W, B], "cuda") +dev = tvm.cuda(0) +a_np = np.random.uniform(size=(in_size, in_size, in_channel, batch)).astype(A.dtype) +w_np = np.random.uniform(size=(kernel, kernel, in_channel, out_channel)).astype(W.dtype) +a = tvm.nd.array(a_np, dev) +w = tvm.nd.array(w_np, dev) +b = tvm.nd.array(np.zeros((out_size, out_size, out_channel, batch), dtype=B.dtype), dev) +func(a, w, b) +evaluator = func.time_evaluator(func.entry_name, dev, number=1) +print("Convolution: %f ms" % (evaluator(a, w, b).mean * 1e3)) +``` + +输出结果: + +``` bash +Convolution: 54.146944 ms +``` + +[下载 Python 源代码:opt_conv_cuda.py](https://tvm.apache.org/docs/_downloads/3c5c85c3954f3110f16ca084e286f03a/opt_conv_cuda.py) + +[下载 Jupyter notebook:opt_conv_cuda.ipynb](https://tvm.apache.org/docs/_downloads/854257a66df713b1f3f82eb3577f95e3/opt_conv_cuda.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/optimize/03-tensorcore_conv.md b/versioned_docs/version-0.12.0/how_to/optimize/03-tensorcore_conv.md new file mode 100644 index 00000000..0a26b12c --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/optimize/03-tensorcore_conv.md @@ -0,0 +1,546 @@ +--- +title: 如何使用 TensorCores 优化卷积 +--- + +# 如何使用 TensorCores 优化卷积 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/optimize_operators/opt_conv_tensorcore.html#sphx-glr-download-how-to-optimize-operators-opt-conv-tensorcore-py) 下载完整的示例代码 +::: + +**作者**:[Siyuan Feng](https://github.com/Hzfengsy) + +本教程演示如何在 TVM 中使用 TensorCores 编写高性能卷积调度。在这个例子中,会假设卷积输入的 batch 较大。强烈建议前置讲解 [如何在 GPU 上优化卷积](gpu_conv)。 + +## TensorCore 介绍 + +每个 Tensor Core 都提供了一个 4x4x4 矩阵处理数组,它使得 `D = A * B + C`,其中 A、B、C 和 D 是 4x4 矩阵,如图所示。矩阵乘法输入 A 和 B 是 FP16 矩阵,而累加矩阵 C 和 D 可以是 FP16 或 FP32 矩阵。 + +但是,CUDA 开发者只能使用 warp 级原语 `wmma::mma_sync(acc_frag, a_frag, b_frag, acc_frag)` 在张量核上执行 16x16x16 半精度矩阵乘法。调用矩阵乘法之前,开发者必须使用原始 `wmma::load_matrix_sync` 显式地将数据从内存加载到寄存器中。NVCC 编译器将该原语转换为多个内存加载指令。在运行时,每个线程从矩阵 A 加载 16 个元素,从 B 加载 16 个元素。 + +## 准备和算法 + +对具有 256 个通道和 14 x 14 维度的输入张量使用固定大小。batch size 为 256,卷积过滤器包含 512 个大小为 3 x 3 的过滤器,使用 stride size 为 1 和 padding size 为 1 进行卷积。在示例中,使用 NHWCnc 内存布局。以下代码定义了 TVM 中的卷积算法。 + +``` python +import tvm +from tvm import te +import numpy as np +from tvm.contrib import nvcc + +# 输入和过滤器的大小 +batch_size = 256 +height = 14 +width = 14 +in_channels = 256 +out_channels = 512 +kernel_h = 3 +kernel_w = 3 +pad_h = 1 +pad_w = 1 +stride_h = 1 +stride_w = 1 + +# TensorCore shape +block_size = 16 + +assert batch_size % block_size == 0 +assert in_channels % block_size == 0 +assert out_channels % block_size == 0 + +# 输入特征图:(N,H,W,IC,n,ic) +data_shape = ( + batch_size // block_size, + height, + width, + in_channels // block_size, + block_size, + block_size, +) +# Kernel: (H, W, IC, OC, ic, oc) +kernel_shape = ( + kernel_h, + kernel_w, + in_channels // block_size, + out_channels // block_size, + block_size, + block_size, +) +# 输出特征图:(N, H, W, OC, n, oc) +output_shape = ( + batch_size // block_size, + height, + width, + out_channels // block_size, + block_size, + block_size, +) + +# Reduction axes +kh = te.reduce_axis((0, kernel_h), name="kh") +kw = te.reduce_axis((0, kernel_w), name="kw") +ic = te.reduce_axis((0, in_channels // block_size), name="ic") +ii = te.reduce_axis((0, block_size), name="ii") + +# 算法 +A = te.placeholder(data_shape, name="A", dtype="float16") +W = te.placeholder(kernel_shape, name="W", dtype="float16") +Apad = te.compute( + ( + batch_size // block_size, + height + 2 * pad_h, + width + 2 * pad_w, + in_channels // block_size, + block_size, + block_size, + ), + lambda n, h, w, i, nn, ii: tvm.tir.if_then_else( + tvm.tir.all(h >= pad_h, h - pad_h < height, w >= pad_w, w - pad_w < width), + A[n, h - pad_h, w - pad_w, i, nn, ii], + tvm.tir.const(0.0, "float16"), + ), + name="Apad", +) +Conv = te.compute( + output_shape, + lambda n, h, w, o, nn, oo: te.sum( + Apad[n, h * stride_h + kh, w * stride_w + kw, ic, nn, ii].astype("float32") + * W[kh, kw, ic, o, ii, oo].astype("float32"), + axis=[ic, kh, kw, ii], + ), + name="Conv", +) + +s = te.create_schedule(Conv.op) +s[Apad].compute_inline() +``` + +## 内存范围 + +传统的 GPU 调度有全局、共享和本地内存范围。为了支持 TensorCores,添加另外三个特殊的内存范围:`wmma.matrix_a`, `wmma.matrix_b` 和 `wmma.accumulator`。在硬件上,所有片段范围都存储在芯片上寄存器级别,与本地内存相同。 + +``` python +# 指定内存层次结构 +AS = s.cache_read(Apad, "shared", [Conv]) +WS = s.cache_read(W, "shared", [Conv]) +AF = s.cache_read(AS, "wmma.matrix_a", [Conv]) +WF = s.cache_read(WS, "wmma.matrix_b", [Conv]) +ConvF = s.cache_write(Conv, "wmma.accumulator") +``` + +## 定义张量内联函数 + +实际上,TensorCore 是一种特殊的硬件操作。因此,可以只用 tensorize 将一个计算单元替换为 TensorCore 指令。首先需要定义张量内联函数。 + +TensorCore 中有四个基本操作:`fill_fragment`, `load_matrix`, `mma_sync` 和 `store_matrix`。由于 `fill_fragment` 和 `mma_sync` 都用于矩阵乘法,所以可以只写以下三个内联函数。 + +``` python +def intrin_wmma_load_matrix(scope): + n = 16 + A = te.placeholder((n, n), name="A", dtype="float16") + BA = tvm.tir.decl_buffer(A.shape, A.dtype, scope="shared", data_alignment=32, offset_factor=256) + C = te.compute((n, n), lambda i, j: A[i, j], name="C") + BC = tvm.tir.decl_buffer(C.shape, C.dtype, scope=scope, data_alignment=32, offset_factor=256) + + def intrin_func(ins, outs): + ib = tvm.tir.ir_builder.create() + + BA = ins[0] + BC = outs[0] + ib.emit( + tvm.tir.call_intrin( + "handle", + "tir.tvm_load_matrix_sync", + BC.data, + n, + n, + n, + BC.elem_offset // 256, + BA.access_ptr("r"), + n, + "row_major", + ) + ) + return ib.get() + + return te.decl_tensor_intrin(C.op, intrin_func, binds={A: BA, C: BC}) + +def intrin_wmma_gemm(): + n = 16 + A = te.placeholder((n, n), name="A", dtype="float16") + B = te.placeholder((n, n), name="B", dtype="float16") + k = te.reduce_axis((0, n), name="k") + C = te.compute( + (n, n), + lambda ii, jj: te.sum(A[ii, k].astype("float") * B[k, jj].astype("float"), axis=k), + name="C", + ) + BA = tvm.tir.decl_buffer( + A.shape, A.dtype, name="BA", scope="wmma.matrix_a", data_alignment=32, offset_factor=256 + ) + BB = tvm.tir.decl_buffer( + B.shape, B.dtype, name="BB", scope="wmma.matrix_b", data_alignment=32, offset_factor=256 + ) + BC = tvm.tir.decl_buffer( + C.shape, C.dtype, name="BC", scope="wmma.accumulator", data_alignment=32, offset_factor=256 + ) + + def intrin_func(ins, outs): + BA, BB = ins + (BC,) = outs + + def init(): + ib = tvm.tir.ir_builder.create() + ib.emit( + tvm.tir.call_intrin( + "handle", "tir.tvm_fill_fragment", BC.data, n, n, n, BC.elem_offset // 256, 0.0 + ) + ) + return ib.get() + + def update(): + ib = tvm.tir.ir_builder.create() + ib.emit( + tvm.tir.call_intrin( + "handle", + "tir.tvm_mma_sync", + BC.data, + BC.elem_offset // 256, + BA.data, + BA.elem_offset // 256, + BB.data, + BB.elem_offset // 256, + BC.data, + BC.elem_offset // 256, + ) + ) + return ib.get() + + return update(), init(), update() + + return te.decl_tensor_intrin(C.op, intrin_func, binds={A: BA, B: BB, C: BC}) + +def intrin_wmma_store_matrix(): + n = 16 + A = te.placeholder((n, n), name="A", dtype="float32") + BA = tvm.tir.decl_buffer( + A.shape, A.dtype, scope="wmma.accumulator", data_alignment=32, offset_factor=256 + ) + C = te.compute((n, n), lambda i, j: A[i, j], name="C") + BC = tvm.tir.decl_buffer(C.shape, C.dtype, scope="global", data_alignment=32, offset_factor=256) + + def intrin_func(ins, outs): + ib = tvm.tir.ir_builder.create() + BA = ins[0] + BC = outs[0] + ib.emit( + tvm.tir.call_intrin( + "handle", + "tir.tvm_store_matrix_sync", + BA.data, + n, + n, + n, + BA.elem_offset // 256, + BC.access_ptr("w"), + n, + "row_major", + ) + ) + return ib.get() + + return te.decl_tensor_intrin(C.op, intrin_func, binds={A: BA, C: BC}) +``` + +## 调度计算 + +要在 TVM 中使用 TensorCores,必须将计算调度到特定的结构中,从而匹配张量内联函数。和传统的 GPU 程序一样,也可以用共享内存来提升速度。如果对分块和共享内存有任何疑问,请参阅 [如何在 GPU 上优化卷积](gpu_conv)。 + +在这个例子中,每个块包含 2x4 个 warp,每个 warp 调用 4x2 TensorCore 指令。因此,每个 warp 的输出 shape 为 64x32,每个块输出 128x128 个 titles。由于共享内存空间的限制,一次只加载 2 个块(2x128x128 tiles)。 + +:::note +*Warp 级操作* + +所有 TensorCore 指令都是 warp 级指令,这意味着一个 warp 中的所有 32 个线程应该同时执行此指令。 + +threadIdx.x extent=32 是解决此问题的最简单方法之一。除了那些直接或间接包含 TensorCore 内联函数的循环,可以将 threadIdx.x 绑定到任何循环。 + +这并不是唯一的解决方案,唯一应该做的就是确保一个 warp 中的所有线程都可以同时调用 TensorCore。 +::: + +``` python +# 定义 tile 大小 +block_row_warps = 4 +block_col_warps = 2 +warp_row_tiles = 2 +warp_col_tiles = 4 +warp_size = 32 +chunk = 2 + +block_x = te.thread_axis("blockIdx.x") +block_y = te.thread_axis("blockIdx.y") +block_z = te.thread_axis("blockIdx.z") +thread_x = te.thread_axis("threadIdx.x") +thread_y = te.thread_axis("threadIdx.y") +thread_z = te.thread_axis("threadIdx.z") + +nc, hc, wc, oc, nnc, ooc = Conv.op.axis +block_k = s[Conv].fuse(hc, wc) +s[Conv].bind(block_k, block_z) +nc, nci = s[Conv].split(nc, factor=warp_row_tiles) +block_i, nc = s[Conv].split(nc, factor=block_row_warps) +oc, oci = s[Conv].split(oc, factor=warp_col_tiles) +block_j, oc = s[Conv].split(oc, factor=block_col_warps) +s[Conv].reorder(block_k, block_i, block_j, nc, oc, nci, oci, nnc, ooc) +s[Conv].bind(block_i, block_x) +s[Conv].bind(block_j, block_y) +s[Conv].bind(nc, thread_y) +s[Conv].bind(oc, thread_z) + +# 调度本地计算 +s[ConvF].compute_at(s[Conv], oc) +n, h, w, o, nnf, oof = ConvF.op.axis +ko, ki = s[ConvF].split(ic, factor=chunk) +s[ConvF].reorder(ko, kh, ki, kw, n, o, nnf, oof, ii) + +# 将中间计算移动到每个输出计算块中 +s[AF].compute_at(s[ConvF], kw) +s[WF].compute_at(s[ConvF], kw) + +# A 的共享内存调度 +s[AS].compute_at(s[ConvF], kh) +n, h, w, i, nn, ii = AS.op.axis +tx, xo = s[AS].split(n, nparts=block_row_warps) +ty, yo = s[AS].split(xo, nparts=block_col_warps) +t = s[AS].fuse(nn, ii) +to, ti = s[AS].split(t, factor=warp_size) +s[AS].bind(tx, thread_y) +s[AS].bind(ty, thread_z) +s[AS].bind(ti, thread_x) + +# W 的共享内存调度 +s[WS].compute_at(s[ConvF], kh) +kh, kw, ic, o, ii, oo = WS.op.axis +tx, xo = s[WS].split(o, nparts=block_row_warps) +ty, yo = s[WS].split(xo, nparts=block_col_warps) +t = s[WS].fuse(ii, oo) +to, ti = s[WS].split(t, nparts=warp_size) +s[WS].bind(tx, thread_y) +s[WS].bind(ty, thread_z) +s[WS].bind(to, thread_x) +s[WS].vectorize(ti) +print(tvm.lower(s, [A, W, Conv], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, W_1: handle, Conv_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float16), float16, [12845056], []), + W: Buffer(W_2: Pointer(float16), float16, [1179648], []), + Conv: Buffer(Conv_2: Pointer(float32), float32, [25690112], [])} + buffer_map = {A_1: A, W_1: W, Conv_1: Conv} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float16, [16, 14, 14, 16, 16, 16], []), W_1: W_3: Buffer(W_2, float16, [3, 3, 16, 32, 16, 16], []), Conv_1: Conv_3: Buffer(Conv_2, float32, [16, 14, 14, 32, 16, 16], [])} { + attr [IterVar(blockIdx.z: int32, (nullptr), "ThreadIndex", "blockIdx.z")] "thread_extent" = 196; + allocate(Conv.wmma.accumulator: Pointer(wmma.accumulator float32), float32, [2048]), storage_scope = wmma.accumulator; + allocate(Apad.shared: Pointer(shared float16), float16, [12288]), storage_scope = shared; + allocate(W.shared: Pointer(shared float16), float16, [12288]), storage_scope = shared; + allocate(Apad.shared.wmma.matrix_a: Pointer(wmma.matrix_a float16), float16, [512]), storage_scope = wmma.matrix_a; + allocate(W.shared.wmma.matrix_b: Pointer(wmma.matrix_b float16), float16, [1024]), storage_scope = wmma.matrix_b; + attr [IterVar(blockIdx.x: int32, (nullptr), "ThreadIndex", "blockIdx.x")] "thread_extent" = 2; + attr [IterVar(blockIdx.y: int32, (nullptr), "ThreadIndex", "blockIdx.y")] "thread_extent" = 4; + attr [IterVar(threadIdx.y: int32, (nullptr), "ThreadIndex", "threadIdx.y")] "thread_extent" = 4; + attr [IterVar(threadIdx.z: int32, (nullptr), "ThreadIndex", "threadIdx.z")] "thread_extent" = 2 { + for (n.c.init: int32, 0, 2) { + for (o.c.init: int32, 0, 4) { + for (nn.c.init: int32, 0, 16) { + for (oo.c.init: int32, 0, 16) { + Conv.wmma.accumulator_1: Buffer(Conv.wmma.accumulator, float32, [2048], [], scope="wmma.accumulator")[((((n.c.init*1024) + (o.c.init*256)) + (nn.c.init*16)) + oo.c.init)] = 0f32 + } + } + } + } + for (ic.outer: int32, 0, 8) { + for (kh: int32, 0, 3) { + for (ax2: int32, 0, 3) { + for (ax3: int32, 0, 2) { + for (ax4.ax5.fused.outer: int32, 0, 8) { + let cse_var_2: int32 = (ax3*256) + let cse_var_1: int32 = (ax4.ax5.fused.outer*32) + attr [IterVar(threadIdx.x: int32, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 32; + Apad.shared_1: Buffer(Apad.shared, float16, [12288], [], scope="shared")[((((((threadIdx.y*3072) + (threadIdx.z*1536)) + (ax2*512)) + cse_var_2) + cse_var_1) + threadIdx.x)] = @tir.if_then_else(((((1 <= (floordiv(blockIdx.z, 14) + kh)) && ((floordiv(blockIdx.z, 14) + kh) < 15)) && (1 <= (ax2 + floormod(blockIdx.z, 14)))) && ((ax2 + floormod(blockIdx.z, 14)) < 15)), A[(((((((((((blockIdx.x*6422528) + (threadIdx.y*1605632)) + (threadIdx.z*802816)) + (kh*57344)) + (blockIdx.z*4096)) + (ax2*4096)) + (ic.outer*512)) + cse_var_2) + cse_var_1) + threadIdx.x) - 61440)], 0f16, dtype=float16) + } + } + } + for (ax1: int32, 0, 3) { + for (ax2_1: int32, 0, 2) { + attr [IterVar(threadIdx.x, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 32; + W.shared_1: Buffer(W.shared, float16, [12288], [], scope="shared")[ramp((((((ax1*4096) + (ax2_1*2048)) + (threadIdx.y*512)) + (threadIdx.z*256)) + (threadIdx.x*8)), 1, 8)] = W[ramp(((((((((kh*393216) + (ax1*131072)) + (ic.outer*16384)) + (ax2_1*8192)) + (blockIdx.y*2048)) + (threadIdx.y*512)) + (threadIdx.z*256)) + (threadIdx.x*8)), 1, 8)] + } + } + for (ic.inner: int32, 0, 2) { + for (kw: int32, 0, 3) { + for (ax0: int32, 0, 2) { + for (ax4: int32, 0, 16) { + for (ax5: int32, 0, 16) { + let cse_var_3: int32 = (ax4*16) + Apad.shared.wmma.matrix_a_1: Buffer(Apad.shared.wmma.matrix_a, float16, [512], [], scope="wmma.matrix_a")[(((ax0*256) + cse_var_3) + ax5)] = Apad.shared_1[((((((threadIdx.y*3072) + (ax0*1536)) + (kw*512)) + (ic.inner*256)) + cse_var_3) + ax5)] + } + } + } + for (ax3_1: int32, 0, 4) { + for (ax4_1: int32, 0, 16) { + for (ax5_1: int32, 0, 16) { + let cse_var_5: int32 = (ax3_1*256) + let cse_var_4: int32 = (ax4_1*16) + W.shared.wmma.matrix_b_1: Buffer(W.shared.wmma.matrix_b, float16, [1024], [], scope="wmma.matrix_b")[((cse_var_5 + cse_var_4) + ax5_1)] = W.shared_1[((((((kw*4096) + (ic.inner*2048)) + (threadIdx.z*1024)) + cse_var_5) + cse_var_4) + ax5_1)] + } + } + } + for (n.c: int32, 0, 2) { + for (o.c: int32, 0, 4) { + for (nn.c: int32, 0, 16) { + for (oo.c: int32, 0, 16) { + for (ii: int32, 0, 16) { + let cse_var_8: int32 = (o.c*256) + let cse_var_7: int32 = (nn.c*16) + let cse_var_6: int32 = ((((n.c*1024) + cse_var_8) + cse_var_7) + oo.c) + Conv.wmma.accumulator_1[cse_var_6] = (Conv.wmma.accumulator_1[cse_var_6] + (cast(float32, Apad.shared.wmma.matrix_a_1[(((n.c*256) + cse_var_7) + ii)])*cast(float32, W.shared.wmma.matrix_b_1[((cse_var_8 + (ii*16)) + oo.c)]))) + } + } + } + } + } + } + } + } + } + for (n.inner: int32, 0, 2) { + for (o.inner: int32, 0, 4) { + for (nn: int32, 0, 16) { + for (oo: int32, 0, 16) { + let cse_var_10: int32 = (o.inner*256) + let cse_var_9: int32 = (nn*16) + Conv[(((((((((blockIdx.x*12845056) + (threadIdx.y*3211264)) + (n.inner*1605632)) + (blockIdx.z*8192)) + (blockIdx.y*2048)) + (threadIdx.z*1024)) + cse_var_10) + cse_var_9) + oo)] = Conv.wmma.accumulator_1[((((n.inner*1024) + cse_var_10) + cse_var_9) + oo)] + } + } + } + } + } +} +``` + +## 将计算降级为内联函数 + +最后一个阶段将计算循环降级到 TensorCore 硬件内联函数,这是通过将 2D 卷积映射到张量内联函数实现的。 + +``` python +s[AF].tensorize(AF.op.axis[-2], intrin_wmma_load_matrix("wmma.matrix_a")) +s[WF].tensorize(WF.op.axis[-2], intrin_wmma_load_matrix("wmma.matrix_b")) +s[Conv].tensorize(nnc, intrin_wmma_store_matrix()) +s[ConvF].tensorize(nnf, intrin_wmma_gemm()) +print(tvm.lower(s, [A, W, Conv], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, W_1: handle, Conv_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float16), float16, [12845056], []), + W: Buffer(W_2: Pointer(float16), float16, [1179648], []), + Conv: Buffer(Conv_2: Pointer(float32), float32, [25690112], [])} + buffer_map = {A_1: A, W_1: W, Conv_1: Conv} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float16, [16, 14, 14, 16, 16, 16], []), W_1: W_3: Buffer(W_2, float16, [3, 3, 16, 32, 16, 16], []), Conv_1: Conv_3: Buffer(Conv_2, float32, [16, 14, 14, 32, 16, 16], [])} { + attr [IterVar(blockIdx.z: int32, (nullptr), "ThreadIndex", "blockIdx.z")] "thread_extent" = 196; + allocate(Conv.wmma.accumulator: Pointer(wmma.accumulator float32), float32, [2048]), storage_scope = wmma.accumulator; + allocate(Apad.shared: Pointer(shared float16), float16, [12288]), storage_scope = shared; + allocate(W.shared: Pointer(shared float16), float16, [12288]), storage_scope = shared; + allocate(Apad.shared.wmma.matrix_a: Pointer(wmma.matrix_a float16), float16, [512]), storage_scope = wmma.matrix_a; + allocate(W.shared.wmma.matrix_b: Pointer(wmma.matrix_b float16), float16, [1024]), storage_scope = wmma.matrix_b; + attr [IterVar(blockIdx.x: int32, (nullptr), "ThreadIndex", "blockIdx.x")] "thread_extent" = 2; + attr [IterVar(blockIdx.y: int32, (nullptr), "ThreadIndex", "blockIdx.y")] "thread_extent" = 4; + attr [IterVar(threadIdx.y: int32, (nullptr), "ThreadIndex", "threadIdx.y")] "thread_extent" = 4; + attr [IterVar(threadIdx.z: int32, (nullptr), "ThreadIndex", "threadIdx.z")] "thread_extent" = 2 { + for (n.c.init: int32, 0, 2) { + for (o.c.init: int32, 0, 4) { + @tir.tvm_fill_fragment(Conv.wmma.accumulator, 16, 16, 16, ((n.c.init*4) + o.c.init), 0f32, dtype=handle) + } + } + for (ic.outer: int32, 0, 8) { + for (kh: int32, 0, 3) { + for (ax2: int32, 0, 3) { + for (ax3: int32, 0, 2) { + for (ax4.ax5.fused.outer: int32, 0, 8) { + let cse_var_2: int32 = (ax3*256) + let cse_var_1: int32 = (ax4.ax5.fused.outer*32) + attr [IterVar(threadIdx.x: int32, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 32; + Apad.shared_1: Buffer(Apad.shared, float16, [12288], [], scope="shared")[((((((threadIdx.y*3072) + (threadIdx.z*1536)) + (ax2*512)) + cse_var_2) + cse_var_1) + threadIdx.x)] = @tir.if_then_else(((((1 <= (floordiv(blockIdx.z, 14) + kh)) && ((floordiv(blockIdx.z, 14) + kh) < 15)) && (1 <= (ax2 + floormod(blockIdx.z, 14)))) && ((ax2 + floormod(blockIdx.z, 14)) < 15)), A[(((((((((((blockIdx.x*6422528) + (threadIdx.y*1605632)) + (threadIdx.z*802816)) + (kh*57344)) + (blockIdx.z*4096)) + (ax2*4096)) + (ic.outer*512)) + cse_var_2) + cse_var_1) + threadIdx.x) - 61440)], 0f16, dtype=float16) + } + } + } + for (ax1: int32, 0, 3) { + for (ax2_1: int32, 0, 2) { + attr [IterVar(threadIdx.x, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 32; + W.shared_1: Buffer(W.shared, float16, [12288], [], scope="shared")[ramp((((((ax1*4096) + (ax2_1*2048)) + (threadIdx.y*512)) + (threadIdx.z*256)) + (threadIdx.x*8)), 1, 8)] = W[ramp(((((((((kh*393216) + (ax1*131072)) + (ic.outer*16384)) + (ax2_1*8192)) + (blockIdx.y*2048)) + (threadIdx.y*512)) + (threadIdx.z*256)) + (threadIdx.x*8)), 1, 8)] + } + } + for (ic.inner: int32, 0, 2) { + for (kw: int32, 0, 3) { + for (ax0: int32, 0, 2) { + @tir.tvm_load_matrix_sync(Apad.shared.wmma.matrix_a, 16, 16, 16, ax0, @tir.tvm_access_ptr(@tir.type_annotation(, dtype=float16), Apad.shared, ((((threadIdx.y*3072) + (ax0*1536)) + (kw*512)) + (ic.inner*256)), 256, 1, dtype=handle), 16, "row_major", dtype=handle) + } + for (ax3_1: int32, 0, 4) { + @tir.tvm_load_matrix_sync(W.shared.wmma.matrix_b, 16, 16, 16, ax3_1, @tir.tvm_access_ptr(@tir.type_annotation(, dtype=float16), W.shared, ((((kw*4096) + (ic.inner*2048)) + (threadIdx.z*1024)) + (ax3_1*256)), 256, 1, dtype=handle), 16, "row_major", dtype=handle) + } + for (n.c: int32, 0, 2) { + for (o.c: int32, 0, 4) { + let cse_var_3: int32 = ((n.c*4) + o.c) + @tir.tvm_mma_sync(Conv.wmma.accumulator, cse_var_3, Apad.shared.wmma.matrix_a, n.c, W.shared.wmma.matrix_b, o.c, Conv.wmma.accumulator, cse_var_3, dtype=handle) + } + } + } + } + } + } + for (n.inner: int32, 0, 2) { + for (o.inner: int32, 0, 4) { + @tir.tvm_store_matrix_sync(Conv.wmma.accumulator, 16, 16, 16, ((n.inner*4) + o.inner), @tir.tvm_access_ptr(@tir.type_annotation(, dtype=float32), Conv_2, (((((((blockIdx.x*12845056) + (threadIdx.y*3211264)) + (n.inner*1605632)) + (blockIdx.z*8192)) + (blockIdx.y*2048)) + (threadIdx.z*1024)) + (o.inner*256)), 256, 2, dtype=handle), 16, "row_major", dtype=handle) + } + } + } +} +``` + +## 生成 CUDA 内核 + +最后使用 TVM 生成和编译 CUDA 内核,并评估卷积的延迟。由于 TensorCores 仅支持 Compute Capability 7.0 或更高版本的 NVIDIA GPU,因此可能无法在构建服务器上运行。 + +``` bash +dev = tvm.cuda(0) +if nvcc.have_tensorcore(dev.compute_version): + with tvm.transform.PassContext(config={"tir.UnrollLoop": {"auto_max_step": 16}}): + func = tvm.build(s, [A, W, Conv], "cuda") + a_np = np.random.uniform(size=data_shape).astype(A.dtype) + w_np = np.random.uniform(size=kernel_shape).astype(W.dtype) + a = tvm.nd.array(a_np, dev) + w = tvm.nd.array(w_np, dev) + c = tvm.nd.array(np.zeros(output_shape, dtype=Conv.dtype), dev) + evaluator = func.time_evaluator(func.entry_name, dev, number=10) + print("conv2d with tensor core: %f ms" % (evaluator(a, w, c).mean * 1e3)) +``` + +输出结果: + +``` bash +conv2d with tensor core: 6.835711 ms +``` + +## 总结 + +本教程演示如何用 TVM 调度原语在特定 GPU 上调用 TensorCore。 + +[下载 Python 源代码:opt_conv_tensorcore.py](https://tvm.apache.org/docs/_downloads/7372db5919b5619bc34fde3434862bca/opt_conv_tensorcore.py) + +[下载 Jupyter Notebook:opt_conv_tensorcore.ipynb](https://tvm.apache.org/docs/_downloads/7455981870c23c8c76482dedf33d8a42/opt_conv_tensorcore.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/optimize/_category_.json b/versioned_docs/version-0.12.0/how_to/optimize/_category_.json new file mode 100644 index 00000000..0f4d2330 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/optimize/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 150 +} diff --git a/versioned_docs/version-0.12.0/how_to/optimize/index.md b/versioned_docs/version-0.12.0/how_to/optimize/index.md new file mode 100644 index 00000000..1baa0b1a --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/optimize/index.md @@ -0,0 +1,9 @@ +--- +title: 优化张量算子 +--- + +以下操作指南演示如何针对不同的 target 优化各种张量操作: + +* [如何在 CPU 上优化 GEMM](cpu_conv) +* [如何在 GPU 上优化卷积](gpu_conv) +* [如何使用 TensorCores 优化卷积](tensorcore_conv) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/relay/01-build_network.md b/versioned_docs/version-0.12.0/how_to/relay/01-build_network.md new file mode 100644 index 00000000..6efa26b4 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/relay/01-build_network.md @@ -0,0 +1,429 @@ +--- +title: 构建图卷积网络 +--- + +# 构建图卷积网络 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/work_with_relay/build_gcn.html#sphx-glr-download-how-to-work-with-relay-build-gcn-py) 下载完整的示例代码 +::: + +**作者**:[Yulun Yao](https://yulunyao.io/),[Chien-Yu Lin](https://homes.cs.washington.edu/\~cyulin/) + +本文介绍如何用 Relay 构建图卷积网络(GCN)。本教程演示在 Cora 数据集上运行 GCN。Cora 数据集是图神经网络(GNN)的 benchmark,同时是支持 GNN 训练和推理的框架。我们直接从 DGL 库加载数据集来与 DGL 进行同类比较。 + +有关 DGL 安装,参阅 [DGL 文档](https://docs.dgl.ai/install/index.html)。 + +有关 PyTorch 安装,参阅 [PyTorch 指南](https://pytorch.org/get-started/locally/)。 + +## 使用 PyTorch 后端在 DGL 中定义 GCN + +这部分重用了 [DGL 示例](https://github.com/dmlc/dgl/tree/master/examples/pytorch/gcn) 的代码。 + +``` python +import torch +import torch.nn as nn +import torch.nn.functional as F +import dgl +import networkx as nx +from dgl.nn.pytorch import GraphConv + +class GCN(nn.Module): + def __init__(self, g, n_infeat, n_hidden, n_classes, n_layers, activation): + super(GCN, self).__init__() + self.g = g + self.layers = nn.ModuleList() + self.layers.append(GraphConv(n_infeat, n_hidden, activation=activation)) + for i in range(n_layers - 1): + self.layers.append(GraphConv(n_hidden, n_hidden, activation=activation)) + self.layers.append(GraphConv(n_hidden, n_classes)) + + def forward(self, features): + h = features + for i, layer in enumerate(self.layers): + # handle api changes for differnt DGL version + # 处理不同 DGL 版本的不同函数 + if dgl.__version__ > "0.3": + h = layer(self.g, h) + else: + h = layer(h, self.g) + return h +``` + +输出结果: + +``` bash +Using backend: pytorch +``` + +## 定义加载数据集和评估准确性的函数 + +可以将这部分替换为你自己的数据集,本示例中,我们选择从 DGL 加载数据: + +``` python +from dgl.data import load_data +from collections import namedtuple + +def load_dataset(dataset="cora"): + args = namedtuple("args", ["dataset"]) + data = load_data(args(dataset)) + + # 删除自循环,避免重复将节点的特征传递给自身 + g = data.graph + g.remove_edges_from(nx.selfloop_edges(g)) + g.add_edges_from(zip(g.nodes, g.nodes)) + + return g, data + +def evaluate(data, logits): + test_mask = data.test_mask # 未包含在训练阶段的测试集 + + pred = logits.argmax(axis=1) + acc = ((pred == data.labels) * test_mask).sum() / test_mask.sum() + + return acc +``` + +## 加载数据并设置模型参数 + +``` python +""" +Parameters +---------- +dataset: str + Name of dataset. You can choose from ['cora', 'citeseer', 'pubmed']. + +num_layer: int + number of hidden layers + +num_hidden: int + number of the hidden units in the hidden layer + +infeat_dim: int + dimension of the input features + +num_classes: int + dimension of model output (Number of classes) +""" + +dataset = "cora" +g, data = load_dataset(dataset) + +num_layers = 1 +num_hidden = 16 +infeat_dim = data.features.shape[1] +num_classes = data.num_labels +``` + +输出结果: + +``` bash +Downloading /workspace/.dgl/cora_v2.zip from https://data.dgl.ai/dataset/cora_v2.zip... +Extracting file to /workspace/.dgl/cora_v2 +Finished data loading and preprocessing. + NumNodes: 2708 + NumEdges: 10556 + NumFeats: 1433 + NumClasses: 7 + NumTrainingSamples: 140 + NumValidationSamples: 500 + NumTestSamples: 1000 +Done saving data into cached files. +/usr/local/lib/python3.7/dist-packages/dgl/data/utils.py:286: UserWarning: Property dataset.graph will be deprecated, please use dataset[0] instead. + warnings.warn('Property {} will be deprecated, please use {} instead.'.format(old, new)) +/usr/local/lib/python3.7/dist-packages/dgl/data/utils.py:286: UserWarning: Property dataset.feat will be deprecated, please use g.ndata['feat'] instead. + warnings.warn('Property {} will be deprecated, please use {} instead.'.format(old, new)) +/usr/local/lib/python3.7/dist-packages/dgl/data/utils.py:286: UserWarning: Property dataset.num_labels will be deprecated, please use dataset.num_classes instead. + warnings.warn('Property {} will be deprecated, please use {} instead.'.format(old, new)) +``` + +## 设置 DGL-PyTorch 模型以取得最好的结果 + +用 https://github.com/dmlc/dgl/blob/master/examples/pytorch/gcn/train.py 训练权重。 + +``` python +from tvm.contrib.download import download_testdata +from dgl import DGLGraph + +features = torch.FloatTensor(data.features) +dgl_g = DGLGraph(g) + +torch_model = GCN(dgl_g, infeat_dim, num_hidden, num_classes, num_layers, F.relu) + +# 下载预训练的权重 +model_url = "https://homes.cs.washington.edu/~cyulin/media/gnn_model/gcn_%s.torch" % (dataset) +model_path = download_testdata(model_url, "gcn_%s.pickle" % (dataset), module="gcn_model") + +# 将 weights 加载到模型中 +torch_model.load_state_dict(torch.load(model_path)) +``` + +输出结果: + +``` bash +/usr/local/lib/python3.7/dist-packages/dgl/data/utils.py:286: UserWarning: Property dataset.feat will be deprecated, please use g.ndata['feat'] instead. + warnings.warn('Property {} will be deprecated, please use {} instead.'.format(old, new)) +/usr/local/lib/python3.7/dist-packages/dgl/base.py:45: DGLWarning: Recommend creating graphs by `dgl.graph(data)` instead of `dgl.DGLGraph(data)`. + return warnings.warn(message, category=category, stacklevel=1) + + +``` + +## 运行 DGL 模型并测试准确性 + +``` python +torch_model.eval() +with torch.no_grad(): + logits_torch = torch_model(features) +print("Print the first five outputs from DGL-PyTorch execution\n", logits_torch[:5]) + +acc = evaluate(data, logits_torch.numpy()) +print("Test accuracy of DGL results: {:.2%}".format(acc)) +``` + +输出结果: + +``` bash +Print the first five outputs from DGL-PyTorch execution + tensor([[-0.2198, -0.7980, 0.0784, 0.9232, -0.9319, -0.7733, 0.9410], + [-0.4646, -0.6606, -0.1732, 1.1829, -0.3705, -0.5535, 0.0858], + [-0.0031, -0.4156, 0.0175, 0.4765, -0.5887, -0.3609, 0.2278], + [-0.8559, -0.8860, 1.4782, 0.9262, -1.3100, -1.0960, -0.0908], + [-0.0702, -1.1651, 1.1453, -0.3586, -0.4938, -0.2288, 0.1827]]) +/usr/local/lib/python3.7/dist-packages/dgl/data/utils.py:286: UserWarning: Property dataset.test_mask will be deprecated, please use g.ndata['test_mask'] instead. + warnings.warn('Property {} will be deprecated, please use {} instead.'.format(old, new)) +/usr/local/lib/python3.7/dist-packages/dgl/data/utils.py:286: UserWarning: Property dataset.label will be deprecated, please use g.ndata['label'] instead. + warnings.warn('Property {} will be deprecated, please use {} instead.'.format(old, new)) +Test accuracy of DGL results: 10.00% +``` + +## 在 Relay 中定义图卷积层 + +在 TVM 上运行 GCN 之前,首先实现 Graph Convolution Layer。参考 https://github.com/dmlc/dgl/blob/master/python/dgl/nn/mxnet/conv/graphconv.py 了解在 DGL 中使用 MXNet 后端实现的 GraphConv 层的更多信息。 + +该层由以下操作定义。注意:我们用两个转置来保持 sparse_dense 算子右侧的邻接矩阵,此方法是临时的,接下来几周内会更新稀疏矩阵转置,使得支持左稀疏算子。 + +$$ +GraphConv(A,H,W)=A∗H∗W= ((H∗W)^{t}∗A^{t})^{t} = (( W^{t} ∗ H^{t})∗ A^{t} )^{t} +$$ + +``` python +from tvm import relay +from tvm.contrib import graph_executor +import tvm +from tvm import te + +def GraphConv(layer_name, input_dim, output_dim, adj, input, norm=None, bias=True, activation=None): + """ + 参数 + ---------- + layer_name: str + 图层名称 + + input_dim: int + 每个节点特征的输入维度 + + output_dim: int, + 每个节点特征的输出维度 + + adj: namedtuple, + 稀疏格式的图形表示(邻接矩阵)(`data`,`indices`,`indptr`),其中`data`的 shape 为[num_nonzeros],indices`的 shape 为[num_nonzeros],`indptr`的 shape 为[num_nodes + 1] + + input: relay.Expr, + shape 为 [num_nodes, input_dim] 的当前层的输入特征 + + norm: relay.Expr, + 范数传给该层,对卷积前后的特征进行归一化。 + + bias: bool + 将 bias 设置为 True,在处理 GCN 层时添加偏差 + + activation: , + 激活函数适用于输出,例如 relay.nn.{relu,sigmoid,log_softmax,softmax,leaky_relu} + + 返回 + ---------- + 输出:tvm.relay.Expr + 该层的输出张量 [num_nodes, output_dim] + """ + if norm is not None: + input = relay.multiply(input, norm) + + weight = relay.var(layer_name + ".weight", shape=(input_dim, output_dim)) + weight_t = relay.transpose(weight) + dense = relay.nn.dense(weight_t, input) + output = relay.nn.sparse_dense(dense, adj) + output_t = relay.transpose(output) + if norm is not None: + output_t = relay.multiply(output_t, norm) + if bias is True: + _bias = relay.var(layer_name + ".bias", shape=(output_dim, 1)) + output_t = relay.nn.bias_add(output_t, _bias, axis=-1) + if activation is not None: + output_t = activation(output_t) + return output_t +``` + +## 准备 GraphConv 层所需的参数 + +``` python +import numpy as np +import networkx as nx + +def prepare_params(g, data): + params = {} + params["infeats"] = data.features.numpy().astype( + "float32" + ) # 目前仅支持 float32 格式 + + # 生成邻接矩阵 + adjacency = nx.to_scipy_sparse_matrix(g) + params["g_data"] = adjacency.data.astype("float32") + params["indices"] = adjacency.indices.astype("int32") + params["indptr"] = adjacency.indptr.astype("int32") + + # 标准化 w.r.t.节点的度 + degs = [g.in_degree[i] for i in range(g.number_of_nodes())] + params["norm"] = np.power(degs, -0.5).astype("float32") + params["norm"] = params["norm"].reshape((params["norm"].shape[0], 1)) + + return params + +params = prepare_params(g, data) + +# 检查特征的 shape 和邻接矩阵的有效性 +assert len(params["infeats"].shape) == 2 +assert ( + params["g_data"] is not None and params["indices"] is not None and params["indptr"] is not None +) +assert params["infeats"].shape[0] == params["indptr"].shape[0] - 1 +``` + +输出结果: + +``` bash +/usr/local/lib/python3.7/dist-packages/dgl/data/utils.py:286: UserWarning: Property dataset.feat will be deprecated, please use g.ndata['feat'] instead. + warnings.warn('Property {} will be deprecated, please use {} instead.'.format(old, new)) +``` + +## 逐层叠加 + +``` python +# 在 Relay 中定义输入特征、范数、邻接矩阵 +infeats = relay.var("infeats", shape=data.features.shape) +norm = relay.Constant(tvm.nd.array(params["norm"])) +g_data = relay.Constant(tvm.nd.array(params["g_data"])) +indices = relay.Constant(tvm.nd.array(params["indices"])) +indptr = relay.Constant(tvm.nd.array(params["indptr"])) + +Adjacency = namedtuple("Adjacency", ["data", "indices", "indptr"]) +adj = Adjacency(g_data, indices, indptr) + +# 构建 2 层 GCN +layers = [] +layers.append( + GraphConv( + layer_name="layers.0", + input_dim=infeat_dim, + output_dim=num_hidden, + adj=adj, + input=infeats, + norm=norm, + activation=relay.nn.relu, + ) +) +layers.append( + GraphConv( + layer_name="layers.1", + input_dim=num_hidden, + output_dim=num_classes, + adj=adj, + input=layers[-1], + norm=norm, + activation=None, + ) +) + +# 分析自由变量并生成 Relay 函数 +output = layers[-1] +``` + +输出结果: + +``` bash +/usr/local/lib/python3.7/dist-packages/dgl/data/utils.py:286: UserWarning: Property dataset.feat will be deprecated, please use g.ndata['feat'] instead. + warnings.warn('Property {} will be deprecated, please use {} instead.'.format(old, new)) +``` + +## 使用 TVM 编译和运行 + +将权重从 PyTorch 模型导出到 Python 字典: + +``` python +model_params = {} +for param_tensor in torch_model.state_dict(): + model_params[param_tensor] = torch_model.state_dict()[param_tensor].numpy() + +for i in range(num_layers + 1): + params["layers.%d.weight" % (i)] = model_params["layers.%d.weight" % (i)] + params["layers.%d.bias" % (i)] = model_params["layers.%d.bias" % (i)] + +# 设置 TVM 构建 target +target = "llvm" # 目前只支持 `llvm` 作为目标 + +func = relay.Function(relay.analysis.free_vars(output), output) +func = relay.build_module.bind_params_by_name(func, params) +mod = tvm.IRModule() +mod["main"] = func +# 使用 Relay 构建 +with tvm.transform.PassContext(opt_level=0): # 目前只支持 opt_level=0 + lib = relay.build(mod, target, params=params) + +# 生成图执行器 +dev = tvm.device(target, 0) +m = graph_executor.GraphModule(lib["default"](dev)) +``` + +## 运行 TVM 模型,测试准确性并使用 DGL 进行验证 + +``` python +m.run() +logits_tvm = m.get_output(0).numpy() +print("Print the first five outputs from TVM execution\n", logits_tvm[:5]) + +labels = data.labels +test_mask = data.test_mask + +acc = evaluate(data, logits_tvm) +print("Test accuracy of TVM results: {:.2%}".format(acc)) + +import tvm.testing + +# 使用 DGL 模型验证结果 +tvm.testing.assert_allclose(logits_torch, logits_tvm, atol=1e-3) +``` + +输出结果: + +```plain +Print the first five outputs from TVM execution + [[-0.21976954 -0.7979525 0.07836491 0.9232204 -0.93188703 -0.7732947 + 0.9410008 ] + [-0.4645713 -0.66060466 -0.17316166 1.1828876 -0.37051404 -0.5534965 + 0.08579484] + [-0.00308266 -0.41562504 0.0175378 0.47649348 -0.5886737 -0.3609016 + 0.22782072] + [-0.8559376 -0.8860172 1.4782399 0.9262254 -1.3099641 -1.0960144 + -0.09084877] + [-0.07015878 -1.1651071 1.1452857 -0.35857323 -0.49377596 -0.22878847 + 0.18269953]] +/usr/local/lib/python3.7/dist-packages/dgl/data/utils.py:286: UserWarning: Property dataset.label will be deprecated, please use g.ndata['label'] instead. + warnings.warn('Property {} will be deprecated, please use {} instead.'.format(old, new)) +/usr/local/lib/python3.7/dist-packages/dgl/data/utils.py:286: UserWarning: Property dataset.test_mask will be deprecated, please use g.ndata['test_mask'] instead. + warnings.warn('Property {} will be deprecated, please use {} instead.'.format(old, new)) +Test accuracy of TVM results: 10.00% +``` + +[下载 Python 源代码:build_gcn.py](https://tvm.apache.org/docs/_downloads/dabb6b43ea9ef9d7bd1a3912001deace/build_gcn.py) + +[下载 Jupyter Notebook:build_gcn.ipynb](https://tvm.apache.org/docs/_downloads/825671e45a9bdc4733400384984cd9dd/build_gcn.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/relay/02-extenal_lib.md b/versioned_docs/version-0.12.0/how_to/relay/02-extenal_lib.md new file mode 100644 index 00000000..ec5c9651 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/relay/02-extenal_lib.md @@ -0,0 +1,557 @@ +--- +title: 在 Relay 中使用外部库 +--- + +# 在 Relay 中使用外部库 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/work_with_relay/using_external_lib.html#sphx-glr-download-how-to-work-with-relay-using-external-lib-py) 下载完整的示例代码 +::: + +**作者**:[Masahiro Masuda](https://github.com/masahi),[Truman Tian](https://github.com/SiNZeRo) + +本文介绍如何将 cuDNN 或 cuBLAS 等外部库与 Relay 一起使用。 + +Relay 内部用 TVM 来生成 target-specific 的代码。例如,TVM 使用 CUDA 后端为用户提供的网络中的所有层生成 CUDA 内核。有时也可将各个供应商开发的外部库合并到 Relay 中,TVM 有一种机制可以透明地调用这些库——对于 Relay 用户,只需要设置一个适当的 target 字符串。 + +使用 Relay 的外部库前,用你要用的库构建 TVM。例如,要用 cuDNN,需启用 *cmake/config.cmake* 中的 USE_CUDNN 选项,必要时要指定 cuDNN 头文件和库目录。 + +首先导入 Relay 和 TVM。 + +``` python +import tvm +from tvm import te +import numpy as np +from tvm.contrib import graph_executor as runtime +from tvm import relay +from tvm.relay import testing +import tvm.testing +``` + +## 创建一个简单网络 + +下面创建一个简单网络进行演示,它由 convolution,batch normalization 和 ReLU activation 组成。 + +``` python +out_channels = 16 +batch_size = 1 + +data = relay.var("data", relay.TensorType((batch_size, 3, 224, 224), "float32")) +weight = relay.var("weight") +bn_gamma = relay.var("bn_gamma") +bn_beta = relay.var("bn_beta") +bn_mmean = relay.var("bn_mean") +bn_mvar = relay.var("bn_var") + +simple_net = relay.nn.conv2d( + data=data, weight=weight, kernel_size=(3, 3), channels=out_channels, padding=(1, 1) +) +simple_net = relay.nn.batch_norm(simple_net, bn_gamma, bn_beta, bn_mmean, bn_mvar)[0] +simple_net = relay.nn.relu(simple_net) +simple_net = relay.Function(relay.analysis.free_vars(simple_net), simple_net) + +data_shape = (batch_size, 3, 224, 224) +net, params = testing.create_workload(simple_net) +``` + +## 使用 CUDA 后端构建和运行 + +正常使用 CUDA 后端构建和运行这个网络。设置日志记录级别为 DEBUG,Relay 计算图编译的结果将作为伪代码转储。 + +``` python +import logging + +logging.basicConfig(level=logging.DEBUG) # to dump TVM IR after fusion + +target = "cuda" +lib = relay.build_module.build(net, target, params=params) + +dev = tvm.device(target, 0) +data = np.random.uniform(-1, 1, size=data_shape).astype("float32") +module = runtime.GraphModule(lib["default"](dev)) +module.set_input("data", data) +module.run() +out_shape = (batch_size, out_channels, 224, 224) +out = module.get_output(0, tvm.nd.empty(out_shape)) +out_cuda = out.numpy() +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +生成的伪代码应如下。注意 bias add,batch normalization 和 ReLU activation 是如何融合到卷积核中的。 TVM 从这个表示中生成一个单一的融合内核。 + +``` python +produce tensor { + // attr [iter_var(blockIdx.z, , blockIdx.z)] thread_extent = 1 + // attr [compute] storage_scope = "local" + allocate compute[float32 * 32] + // attr [pad_temp.shared] storage_scope = "shared" + allocate pad_temp.shared[float32 * 180] + // attr [placeholder.shared] storage_scope = "shared" + allocate placeholder.shared[float32 * 144] + // attr [iter_var(blockIdx.y, , blockIdx.y)] thread_extent = 28 + // attr [iter_var(blockIdx.x, , blockIdx.x)] thread_extent = 14 + // attr [iter_var(threadIdx.z, , threadIdx.z)] thread_extent = 4 + // attr [iter_var(threadIdx.y, , threadIdx.y)] thread_extent = 1 + // attr [iter_var(threadIdx.x, , threadIdx.x)] thread_extent = 16 + produce compute { + compute[0] = 0.000000f + compute[1] = 0.000000f + compute[2] = 0.000000f + compute[3] = 0.000000f + compute[4] = 0.000000f + compute[5] = 0.000000f + compute[6] = 0.000000f + compute[7] = 0.000000f + compute[8] = 0.000000f + compute[9] = 0.000000f + compute[10] = 0.000000f + compute[11] = 0.000000f + compute[12] = 0.000000f + compute[13] = 0.000000f + compute[14] = 0.000000f + compute[15] = 0.000000f + compute[16] = 0.000000f + compute[17] = 0.000000f + compute[18] = 0.000000f + compute[19] = 0.000000f + compute[20] = 0.000000f + compute[21] = 0.000000f + compute[22] = 0.000000f + compute[23] = 0.000000f + compute[24] = 0.000000f + compute[25] = 0.000000f + compute[26] = 0.000000f + compute[27] = 0.000000f + compute[28] = 0.000000f + compute[29] = 0.000000f + compute[30] = 0.000000f + compute[31] = 0.000000f + for (rc.outer, 0, 3) { + produce pad_temp.shared { + // attr [iter_var(threadIdx.z, , threadIdx.z)] thread_extent = 4 + // attr [iter_var(threadIdx.y, , threadIdx.y)] thread_extent = 1 + // attr [iter_var(threadIdx.x, , threadIdx.x)] thread_extent = 16 + if (likely(((threadIdx.z*15) < (60 - threadIdx.x)))) { + if (likely((threadIdx.x < 15))) { + pad_temp.shared[(((((threadIdx.z*15) + threadIdx.x)/60)*180) + ((((((threadIdx.z*15) + threadIdx.x)/6) % 10)*18) + ((((threadIdx.z*3) + threadIdx.x)*3) % 18)))] = tvm_if_then_else((((((1 - ((((threadIdx.z*15) + threadIdx.x)/6) % 10)) <= (blockIdx.y*8)) && ((blockIdx.y*8) < (225 - ((((threadIdx.z*15) + threadIdx.x)/6) % 10)))) && ((1 - ((((threadIdx.z*3) + threadIdx.x)*3) % 18)) <= (blockIdx.x*16))) && ((blockIdx.x*16) < (225 - ((((threadIdx.z*3) + threadIdx.x)*3) % 18)))), placeholder[((((((((blockIdx.y*112) + blockIdx.x) + (rc.outer*3136)) + ((((threadIdx.z*15) + threadIdx.x)/60)*9408))*16) + ((((threadIdx.z*3) + threadIdx.x)*3) % 18)) + (((((threadIdx.z*15) + threadIdx.x)/6) % 10)*224)) + -225)], 0.000000f) + pad_temp.shared[(((((((threadIdx.z*15) + threadIdx.x)*3) + 1)/180)*180) + ((((((((threadIdx.z*15) + threadIdx.x)*3) + 1)/18) % 10)*18) + (((((threadIdx.z*3) + threadIdx.x)*3) + 1) % 18)))] = tvm_if_then_else((((((1 - ((((((threadIdx.z*15) + threadIdx.x)*3) + 1)/18) % 10)) <= (blockIdx.y*8)) && ((blockIdx.y*8) < (225 - ((((((threadIdx.z*15) + threadIdx.x)*3) + 1)/18) % 10)))) && ((1 - (((((threadIdx.z*3) + threadIdx.x)*3) + 1) % 18)) <= (blockIdx.x*16))) && ((blockIdx.x*16) < (225 - (((((threadIdx.z*3) + threadIdx.x)*3) + 1) % 18)))), placeholder[((((((((blockIdx.y*112) + blockIdx.x) + (rc.outer*3136)) + ((((((threadIdx.z*15) + threadIdx.x)*3) + 1)/180)*9408))*16) + (((((threadIdx.z*3) + threadIdx.x)*3) + 1) % 18)) + (((((((threadIdx.z*15) + threadIdx.x)*3) + 1)/18) % 10)*224)) + -225)], 0.000000f) + pad_temp.shared[(((((((threadIdx.z*15) + threadIdx.x)*3) + 2)/180)*180) + ((((((((threadIdx.z*15) + threadIdx.x)*3) + 2)/18) % 10)*18) + (((((threadIdx.z*3) + threadIdx.x)*3) + 2) % 18)))] = tvm_if_then_else((((((1 - ((((((threadIdx.z*15) + threadIdx.x)*3) + 2)/18) % 10)) <= (blockIdx.y*8)) && ((blockIdx.y*8) < (225 - ((((((threadIdx.z*15) + threadIdx.x)*3) + 2)/18) % 10)))) && ((1 - (((((threadIdx.z*3) + threadIdx.x)*3) + 2) % 18)) <= (blockIdx.x*16))) && ((blockIdx.x*16) < (225 - (((((threadIdx.z*3) + threadIdx.x)*3) + 2) % 18)))), placeholder[((((((((blockIdx.y*112) + blockIdx.x) + (rc.outer*3136)) + ((((((threadIdx.z*15) + threadIdx.x)*3) + 2)/180)*9408))*16) + (((((threadIdx.z*3) + threadIdx.x)*3) + 2) % 18)) + (((((((threadIdx.z*15) + threadIdx.x)*3) + 2)/18) % 10)*224)) + -225)], 0.000000f) + } + } + } + produce placeholder.shared { + // attr [iter_var(threadIdx.z, , threadIdx.z)] thread_extent = 4 + // attr [iter_var(threadIdx.y, , threadIdx.y)] thread_extent = 1 + // attr [iter_var(threadIdx.x, , threadIdx.x)] thread_extent = 16 + if (likely(((threadIdx.z*4) < (16 - (threadIdx.x/3))))) { + if (likely(((threadIdx.z*12) < (48 - threadIdx.x)))) { + if (likely((threadIdx.x < 12))) { + placeholder.shared[(((((threadIdx.z*4) + (threadIdx.x/3))*3) + (threadIdx.x % 3))*3)] = placeholder[(((((rc.outer + (threadIdx.z*12)) + ((threadIdx.x/3)*3))*3) + (threadIdx.x % 3))*3)] + placeholder.shared[((((((threadIdx.z*4) + (threadIdx.x/3))*3) + (threadIdx.x % 3))*3) + 1)] = placeholder[((((((rc.outer + (threadIdx.z*12)) + ((threadIdx.x/3)*3))*3) + (threadIdx.x % 3))*3) + 1)] + placeholder.shared[((((((threadIdx.z*4) + (threadIdx.x/3))*3) + (threadIdx.x % 3))*3) + 2)] = placeholder[((((((rc.outer + (threadIdx.z*12)) + ((threadIdx.x/3)*3))*3) + (threadIdx.x % 3))*3) + 2)] + } + } + } + } + compute[0] = (compute[0] + (pad_temp.shared[threadIdx.x]*placeholder.shared[(threadIdx.z*36)])) + compute[1] = (compute[1] + (pad_temp.shared[(threadIdx.x + 18)]*placeholder.shared[(threadIdx.z*36)])) + compute[2] = (compute[2] + (pad_temp.shared[(threadIdx.x + 36)]*placeholder.shared[(threadIdx.z*36)])) + compute[3] = (compute[3] + (pad_temp.shared[(threadIdx.x + 54)]*placeholder.shared[(threadIdx.z*36)])) + compute[4] = (compute[4] + (pad_temp.shared[(threadIdx.x + 72)]*placeholder.shared[(threadIdx.z*36)])) + compute[5] = (compute[5] + (pad_temp.shared[(threadIdx.x + 90)]*placeholder.shared[(threadIdx.z*36)])) + compute[6] = (compute[6] + (pad_temp.shared[(threadIdx.x + 108)]*placeholder.shared[(threadIdx.z*36)])) + compute[7] = (compute[7] + (pad_temp.shared[(threadIdx.x + 126)]*placeholder.shared[(threadIdx.z*36)])) + compute[8] = (compute[8] + (pad_temp.shared[threadIdx.x]*placeholder.shared[((threadIdx.z*36) + 9)])) + compute[9] = (compute[9] + (pad_temp.shared[(threadIdx.x + 18)]*placeholder.shared[((threadIdx.z*36) + 9)])) + compute[10] = (compute[10] + (pad_temp.shared[(threadIdx.x + 36)]*placeholder.shared[((threadIdx.z*36) + 9)])) + compute[11] = (compute[11] + (pad_temp.shared[(threadIdx.x + 54)]*placeholder.shared[((threadIdx.z*36) + 9)])) + compute[12] = (compute[12] + (pad_temp.shared[(threadIdx.x + 72)]*placeholder.shared[((threadIdx.z*36) + 9)])) + compute[13] = (compute[13] + (pad_temp.shared[(threadIdx.x + 90)]*placeholder.shared[((threadIdx.z*36) + 9)])) + compute[14] = (compute[14] + (pad_temp.shared[(threadIdx.x + 108)]*placeholder.shared[((threadIdx.z*36) + 9)])) + compute[15] = (compute[15] + (pad_temp.shared[(threadIdx.x + 126)]*placeholder.shared[((threadIdx.z*36) + 9)])) + compute[16] = (compute[16] + (pad_temp.shared[threadIdx.x]*placeholder.shared[((threadIdx.z*36) + 18)])) + compute[17] = (compute[17] + (pad_temp.shared[(threadIdx.x + 18)]*placeholder.shared[((threadIdx.z*36) + 18)])) + compute[18] = (compute[18] + (pad_temp.shared[(threadIdx.x + 36)]*placeholder.shared[((threadIdx.z*36) + 18)])) + compute[19] = (compute[19] + (pad_temp.shared[(threadIdx.x + 54)]*placeholder.shared[((threadIdx.z*36) + 18)])) + compute[20] = (compute[20] + (pad_temp.shared[(threadIdx.x + 72)]*placeholder.shared[((threadIdx.z*36) + 18)])) + compute[21] = (compute[21] + (pad_temp.shared[(threadIdx.x + 90)]*placeholder.shared[((threadIdx.z*36) + 18)])) + compute[22] = (compute[22] + (pad_temp.shared[(threadIdx.x + 108)]*placeholder.shared[((threadIdx.z*36) + 18)])) + compute[23] = (compute[23] + (pad_temp.shared[(threadIdx.x + 126)]*placeholder.shared[((threadIdx.z*36) + 18)])) + compute[24] = (compute[24] + (pad_temp.shared[threadIdx.x]*placeholder.shared[((threadIdx.z*36) + 27)])) + compute[25] = (compute[25] + (pad_temp.shared[(threadIdx.x + 18)]*placeholder.shared[((threadIdx.z*36) + 27)])) + compute[26] = (compute[26] + (pad_temp.shared[(threadIdx.x + 36)]*placeholder.shared[((threadIdx.z*36) + 27)])) + compute[27] = (compute[27] + (pad_temp.shared[(threadIdx.x + 54)]*placeholder.shared[((threadIdx.z*36) + 27)])) + compute[28] = (compute[28] + (pad_temp.shared[(threadIdx.x + 72)]*placeholder.shared[((threadIdx.z*36) + 27)])) + compute[29] = (compute[29] + (pad_temp.shared[(threadIdx.x + 90)]*placeholder.shared[((threadIdx.z*36) + 27)])) + compute[30] = (compute[30] + (pad_temp.shared[(threadIdx.x + 108)]*placeholder.shared[((threadIdx.z*36) + 27)])) + compute[31] = (compute[31] + (pad_temp.shared[(threadIdx.x + 126)]*placeholder.shared[((threadIdx.z*36) + 27)])) + compute[0] = (compute[0] + (pad_temp.shared[(threadIdx.x + 1)]*placeholder.shared[((threadIdx.z*36) + 1)])) + compute[1] = (compute[1] + (pad_temp.shared[(threadIdx.x + 19)]*placeholder.shared[((threadIdx.z*36) + 1)])) + compute[2] = (compute[2] + (pad_temp.shared[(threadIdx.x + 37)]*placeholder.shared[((threadIdx.z*36) + 1)])) + compute[3] = (compute[3] + (pad_temp.shared[(threadIdx.x + 55)]*placeholder.shared[((threadIdx.z*36) + 1)])) + compute[4] = (compute[4] + (pad_temp.shared[(threadIdx.x + 73)]*placeholder.shared[((threadIdx.z*36) + 1)])) + compute[5] = (compute[5] + (pad_temp.shared[(threadIdx.x + 91)]*placeholder.shared[((threadIdx.z*36) + 1)])) + compute[6] = (compute[6] + (pad_temp.shared[(threadIdx.x + 109)]*placeholder.shared[((threadIdx.z*36) + 1)])) + compute[7] = (compute[7] + (pad_temp.shared[(threadIdx.x + 127)]*placeholder.shared[((threadIdx.z*36) + 1)])) + compute[8] = (compute[8] + (pad_temp.shared[(threadIdx.x + 1)]*placeholder.shared[((threadIdx.z*36) + 10)])) + compute[9] = (compute[9] + (pad_temp.shared[(threadIdx.x + 19)]*placeholder.shared[((threadIdx.z*36) + 10)])) + compute[10] = (compute[10] + (pad_temp.shared[(threadIdx.x + 37)]*placeholder.shared[((threadIdx.z*36) + 10)])) + compute[11] = (compute[11] + (pad_temp.shared[(threadIdx.x + 55)]*placeholder.shared[((threadIdx.z*36) + 10)])) + compute[12] = (compute[12] + (pad_temp.shared[(threadIdx.x + 73)]*placeholder.shared[((threadIdx.z*36) + 10)])) + compute[13] = (compute[13] + (pad_temp.shared[(threadIdx.x + 91)]*placeholder.shared[((threadIdx.z*36) + 10)])) + compute[14] = (compute[14] + (pad_temp.shared[(threadIdx.x + 109)]*placeholder.shared[((threadIdx.z*36) + 10)])) + compute[15] = (compute[15] + (pad_temp.shared[(threadIdx.x + 127)]*placeholder.shared[((threadIdx.z*36) + 10)])) + compute[16] = (compute[16] + (pad_temp.shared[(threadIdx.x + 1)]*placeholder.shared[((threadIdx.z*36) + 19)])) + compute[17] = (compute[17] + (pad_temp.shared[(threadIdx.x + 19)]*placeholder.shared[((threadIdx.z*36) + 19)])) + compute[18] = (compute[18] + (pad_temp.shared[(threadIdx.x + 37)]*placeholder.shared[((threadIdx.z*36) + 19)])) + compute[19] = (compute[19] + (pad_temp.shared[(threadIdx.x + 55)]*placeholder.shared[((threadIdx.z*36) + 19)])) + compute[20] = (compute[20] + (pad_temp.shared[(threadIdx.x + 73)]*placeholder.shared[((threadIdx.z*36) + 19)])) + compute[21] = (compute[21] + (pad_temp.shared[(threadIdx.x + 91)]*placeholder.shared[((threadIdx.z*36) + 19)])) + compute[22] = (compute[22] + (pad_temp.shared[(threadIdx.x + 109)]*placeholder.shared[((threadIdx.z*36) + 19)])) + compute[23] = (compute[23] + (pad_temp.shared[(threadIdx.x + 127)]*placeholder.shared[((threadIdx.z*36) + 19)])) + compute[24] = (compute[24] + (pad_temp.shared[(threadIdx.x + 1)]*placeholder.shared[((threadIdx.z*36) + 28)])) + compute[25] = (compute[25] + (pad_temp.shared[(threadIdx.x + 19)]*placeholder.shared[((threadIdx.z*36) + 28)])) + compute[26] = (compute[26] + (pad_temp.shared[(threadIdx.x + 37)]*placeholder.shared[((threadIdx.z*36) + 28)])) + compute[27] = (compute[27] + (pad_temp.shared[(threadIdx.x + 55)]*placeholder.shared[((threadIdx.z*36) + 28)])) + compute[28] = (compute[28] + (pad_temp.shared[(threadIdx.x + 73)]*placeholder.shared[((threadIdx.z*36) + 28)])) + compute[29] = (compute[29] + (pad_temp.shared[(threadIdx.x + 91)]*placeholder.shared[((threadIdx.z*36) + 28)])) + compute[30] = (compute[30] + (pad_temp.shared[(threadIdx.x + 109)]*placeholder.shared[((threadIdx.z*36) + 28)])) + compute[31] = (compute[31] + (pad_temp.shared[(threadIdx.x + 127)]*placeholder.shared[((threadIdx.z*36) + 28)])) + compute[0] = (compute[0] + (pad_temp.shared[(threadIdx.x + 2)]*placeholder.shared[((threadIdx.z*36) + 2)])) + compute[1] = (compute[1] + (pad_temp.shared[(threadIdx.x + 20)]*placeholder.shared[((threadIdx.z*36) + 2)])) + compute[2] = (compute[2] + (pad_temp.shared[(threadIdx.x + 38)]*placeholder.shared[((threadIdx.z*36) + 2)])) + compute[3] = (compute[3] + (pad_temp.shared[(threadIdx.x + 56)]*placeholder.shared[((threadIdx.z*36) + 2)])) + compute[4] = (compute[4] + (pad_temp.shared[(threadIdx.x + 74)]*placeholder.shared[((threadIdx.z*36) + 2)])) + compute[5] = (compute[5] + (pad_temp.shared[(threadIdx.x + 92)]*placeholder.shared[((threadIdx.z*36) + 2)])) + compute[6] = (compute[6] + (pad_temp.shared[(threadIdx.x + 110)]*placeholder.shared[((threadIdx.z*36) + 2)])) + compute[7] = (compute[7] + (pad_temp.shared[(threadIdx.x + 128)]*placeholder.shared[((threadIdx.z*36) + 2)])) + compute[8] = (compute[8] + (pad_temp.shared[(threadIdx.x + 2)]*placeholder.shared[((threadIdx.z*36) + 11)])) + compute[9] = (compute[9] + (pad_temp.shared[(threadIdx.x + 20)]*placeholder.shared[((threadIdx.z*36) + 11)])) + compute[10] = (compute[10] + (pad_temp.shared[(threadIdx.x + 38)]*placeholder.shared[((threadIdx.z*36) + 11)])) + compute[11] = (compute[11] + (pad_temp.shared[(threadIdx.x + 56)]*placeholder.shared[((threadIdx.z*36) + 11)])) + compute[12] = (compute[12] + (pad_temp.shared[(threadIdx.x + 74)]*placeholder.shared[((threadIdx.z*36) + 11)])) + compute[13] = (compute[13] + (pad_temp.shared[(threadIdx.x + 92)]*placeholder.shared[((threadIdx.z*36) + 11)])) + compute[14] = (compute[14] + (pad_temp.shared[(threadIdx.x + 110)]*placeholder.shared[((threadIdx.z*36) + 11)])) + compute[15] = (compute[15] + (pad_temp.shared[(threadIdx.x + 128)]*placeholder.shared[((threadIdx.z*36) + 11)])) + compute[16] = (compute[16] + (pad_temp.shared[(threadIdx.x + 2)]*placeholder.shared[((threadIdx.z*36) + 20)])) + compute[17] = (compute[17] + (pad_temp.shared[(threadIdx.x + 20)]*placeholder.shared[((threadIdx.z*36) + 20)])) + compute[18] = (compute[18] + (pad_temp.shared[(threadIdx.x + 38)]*placeholder.shared[((threadIdx.z*36) + 20)])) + compute[19] = (compute[19] + (pad_temp.shared[(threadIdx.x + 56)]*placeholder.shared[((threadIdx.z*36) + 20)])) + compute[20] = (compute[20] + (pad_temp.shared[(threadIdx.x + 74)]*placeholder.shared[((threadIdx.z*36) + 20)])) + compute[21] = (compute[21] + (pad_temp.shared[(threadIdx.x + 92)]*placeholder.shared[((threadIdx.z*36) + 20)])) + compute[22] = (compute[22] + (pad_temp.shared[(threadIdx.x + 110)]*placeholder.shared[((threadIdx.z*36) + 20)])) + compute[23] = (compute[23] + (pad_temp.shared[(threadIdx.x + 128)]*placeholder.shared[((threadIdx.z*36) + 20)])) + compute[24] = (compute[24] + (pad_temp.shared[(threadIdx.x + 2)]*placeholder.shared[((threadIdx.z*36) + 29)])) + compute[25] = (compute[25] + (pad_temp.shared[(threadIdx.x + 20)]*placeholder.shared[((threadIdx.z*36) + 29)])) + compute[26] = (compute[26] + (pad_temp.shared[(threadIdx.x + 38)]*placeholder.shared[((threadIdx.z*36) + 29)])) + compute[27] = (compute[27] + (pad_temp.shared[(threadIdx.x + 56)]*placeholder.shared[((threadIdx.z*36) + 29)])) + compute[28] = (compute[28] + (pad_temp.shared[(threadIdx.x + 74)]*placeholder.shared[((threadIdx.z*36) + 29)])) + compute[29] = (compute[29] + (pad_temp.shared[(threadIdx.x + 92)]*placeholder.shared[((threadIdx.z*36) + 29)])) + compute[30] = (compute[30] + (pad_temp.shared[(threadIdx.x + 110)]*placeholder.shared[((threadIdx.z*36) + 29)])) + compute[31] = (compute[31] + (pad_temp.shared[(threadIdx.x + 128)]*placeholder.shared[((threadIdx.z*36) + 29)])) + compute[0] = (compute[0] + (pad_temp.shared[(threadIdx.x + 18)]*placeholder.shared[((threadIdx.z*36) + 3)])) + compute[1] = (compute[1] + (pad_temp.shared[(threadIdx.x + 36)]*placeholder.shared[((threadIdx.z*36) + 3)])) + compute[2] = (compute[2] + (pad_temp.shared[(threadIdx.x + 54)]*placeholder.shared[((threadIdx.z*36) + 3)])) + compute[3] = (compute[3] + (pad_temp.shared[(threadIdx.x + 72)]*placeholder.shared[((threadIdx.z*36) + 3)])) + compute[4] = (compute[4] + (pad_temp.shared[(threadIdx.x + 90)]*placeholder.shared[((threadIdx.z*36) + 3)])) + compute[5] = (compute[5] + (pad_temp.shared[(threadIdx.x + 108)]*placeholder.shared[((threadIdx.z*36) + 3)])) + compute[6] = (compute[6] + (pad_temp.shared[(threadIdx.x + 126)]*placeholder.shared[((threadIdx.z*36) + 3)])) + compute[7] = (compute[7] + (pad_temp.shared[(threadIdx.x + 144)]*placeholder.shared[((threadIdx.z*36) + 3)])) + compute[8] = (compute[8] + (pad_temp.shared[(threadIdx.x + 18)]*placeholder.shared[((threadIdx.z*36) + 12)])) + compute[9] = (compute[9] + (pad_temp.shared[(threadIdx.x + 36)]*placeholder.shared[((threadIdx.z*36) + 12)])) + compute[10] = (compute[10] + (pad_temp.shared[(threadIdx.x + 54)]*placeholder.shared[((threadIdx.z*36) + 12)])) + compute[11] = (compute[11] + (pad_temp.shared[(threadIdx.x + 72)]*placeholder.shared[((threadIdx.z*36) + 12)])) + compute[12] = (compute[12] + (pad_temp.shared[(threadIdx.x + 90)]*placeholder.shared[((threadIdx.z*36) + 12)])) + compute[13] = (compute[13] + (pad_temp.shared[(threadIdx.x + 108)]*placeholder.shared[((threadIdx.z*36) + 12)])) + compute[14] = (compute[14] + (pad_temp.shared[(threadIdx.x + 126)]*placeholder.shared[((threadIdx.z*36) + 12)])) + compute[15] = (compute[15] + (pad_temp.shared[(threadIdx.x + 144)]*placeholder.shared[((threadIdx.z*36) + 12)])) + compute[16] = (compute[16] + (pad_temp.shared[(threadIdx.x + 18)]*placeholder.shared[((threadIdx.z*36) + 21)])) + compute[17] = (compute[17] + (pad_temp.shared[(threadIdx.x + 36)]*placeholder.shared[((threadIdx.z*36) + 21)])) + compute[18] = (compute[18] + (pad_temp.shared[(threadIdx.x + 54)]*placeholder.shared[((threadIdx.z*36) + 21)])) + compute[19] = (compute[19] + (pad_temp.shared[(threadIdx.x + 72)]*placeholder.shared[((threadIdx.z*36) + 21)])) + compute[20] = (compute[20] + (pad_temp.shared[(threadIdx.x + 90)]*placeholder.shared[((threadIdx.z*36) + 21)])) + compute[21] = (compute[21] + (pad_temp.shared[(threadIdx.x + 108)]*placeholder.shared[((threadIdx.z*36) + 21)])) + compute[22] = (compute[22] + (pad_temp.shared[(threadIdx.x + 126)]*placeholder.shared[((threadIdx.z*36) + 21)])) + compute[23] = (compute[23] + (pad_temp.shared[(threadIdx.x + 144)]*placeholder.shared[((threadIdx.z*36) + 21)])) + compute[24] = (compute[24] + (pad_temp.shared[(threadIdx.x + 18)]*placeholder.shared[((threadIdx.z*36) + 30)])) + compute[25] = (compute[25] + (pad_temp.shared[(threadIdx.x + 36)]*placeholder.shared[((threadIdx.z*36) + 30)])) + compute[26] = (compute[26] + (pad_temp.shared[(threadIdx.x + 54)]*placeholder.shared[((threadIdx.z*36) + 30)])) + compute[27] = (compute[27] + (pad_temp.shared[(threadIdx.x + 72)]*placeholder.shared[((threadIdx.z*36) + 30)])) + compute[28] = (compute[28] + (pad_temp.shared[(threadIdx.x + 90)]*placeholder.shared[((threadIdx.z*36) + 30)])) + compute[29] = (compute[29] + (pad_temp.shared[(threadIdx.x + 108)]*placeholder.shared[((threadIdx.z*36) + 30)])) + compute[30] = (compute[30] + (pad_temp.shared[(threadIdx.x + 126)]*placeholder.shared[((threadIdx.z*36) + 30)])) + compute[31] = (compute[31] + (pad_temp.shared[(threadIdx.x + 144)]*placeholder.shared[((threadIdx.z*36) + 30)])) + compute[0] = (compute[0] + (pad_temp.shared[(threadIdx.x + 19)]*placeholder.shared[((threadIdx.z*36) + 4)])) + compute[1] = (compute[1] + (pad_temp.shared[(threadIdx.x + 37)]*placeholder.shared[((threadIdx.z*36) + 4)])) + compute[2] = (compute[2] + (pad_temp.shared[(threadIdx.x + 55)]*placeholder.shared[((threadIdx.z*36) + 4)])) + compute[3] = (compute[3] + (pad_temp.shared[(threadIdx.x + 73)]*placeholder.shared[((threadIdx.z*36) + 4)])) + compute[4] = (compute[4] + (pad_temp.shared[(threadIdx.x + 91)]*placeholder.shared[((threadIdx.z*36) + 4)])) + compute[5] = (compute[5] + (pad_temp.shared[(threadIdx.x + 109)]*placeholder.shared[((threadIdx.z*36) + 4)])) + compute[6] = (compute[6] + (pad_temp.shared[(threadIdx.x + 127)]*placeholder.shared[((threadIdx.z*36) + 4)])) + compute[7] = (compute[7] + (pad_temp.shared[(threadIdx.x + 145)]*placeholder.shared[((threadIdx.z*36) + 4)])) + compute[8] = (compute[8] + (pad_temp.shared[(threadIdx.x + 19)]*placeholder.shared[((threadIdx.z*36) + 13)])) + compute[9] = (compute[9] + (pad_temp.shared[(threadIdx.x + 37)]*placeholder.shared[((threadIdx.z*36) + 13)])) + compute[10] = (compute[10] + (pad_temp.shared[(threadIdx.x + 55)]*placeholder.shared[((threadIdx.z*36) + 13)])) + compute[11] = (compute[11] + (pad_temp.shared[(threadIdx.x + 73)]*placeholder.shared[((threadIdx.z*36) + 13)])) + compute[12] = (compute[12] + (pad_temp.shared[(threadIdx.x + 91)]*placeholder.shared[((threadIdx.z*36) + 13)])) + compute[13] = (compute[13] + (pad_temp.shared[(threadIdx.x + 109)]*placeholder.shared[((threadIdx.z*36) + 13)])) + compute[14] = (compute[14] + (pad_temp.shared[(threadIdx.x + 127)]*placeholder.shared[((threadIdx.z*36) + 13)])) + compute[15] = (compute[15] + (pad_temp.shared[(threadIdx.x + 145)]*placeholder.shared[((threadIdx.z*36) + 13)])) + compute[16] = (compute[16] + (pad_temp.shared[(threadIdx.x + 19)]*placeholder.shared[((threadIdx.z*36) + 22)])) + compute[17] = (compute[17] + (pad_temp.shared[(threadIdx.x + 37)]*placeholder.shared[((threadIdx.z*36) + 22)])) + compute[18] = (compute[18] + (pad_temp.shared[(threadIdx.x + 55)]*placeholder.shared[((threadIdx.z*36) + 22)])) + compute[19] = (compute[19] + (pad_temp.shared[(threadIdx.x + 73)]*placeholder.shared[((threadIdx.z*36) + 22)])) + compute[20] = (compute[20] + (pad_temp.shared[(threadIdx.x + 91)]*placeholder.shared[((threadIdx.z*36) + 22)])) + compute[21] = (compute[21] + (pad_temp.shared[(threadIdx.x + 109)]*placeholder.shared[((threadIdx.z*36) + 22)])) + compute[22] = (compute[22] + (pad_temp.shared[(threadIdx.x + 127)]*placeholder.shared[((threadIdx.z*36) + 22)])) + compute[23] = (compute[23] + (pad_temp.shared[(threadIdx.x + 145)]*placeholder.shared[((threadIdx.z*36) + 22)])) + compute[24] = (compute[24] + (pad_temp.shared[(threadIdx.x + 19)]*placeholder.shared[((threadIdx.z*36) + 31)])) + compute[25] = (compute[25] + (pad_temp.shared[(threadIdx.x + 37)]*placeholder.shared[((threadIdx.z*36) + 31)])) + compute[26] = (compute[26] + (pad_temp.shared[(threadIdx.x + 55)]*placeholder.shared[((threadIdx.z*36) + 31)])) + compute[27] = (compute[27] + (pad_temp.shared[(threadIdx.x + 73)]*placeholder.shared[((threadIdx.z*36) + 31)])) + compute[28] = (compute[28] + (pad_temp.shared[(threadIdx.x + 91)]*placeholder.shared[((threadIdx.z*36) + 31)])) + compute[29] = (compute[29] + (pad_temp.shared[(threadIdx.x + 109)]*placeholder.shared[((threadIdx.z*36) + 31)])) + compute[30] = (compute[30] + (pad_temp.shared[(threadIdx.x + 127)]*placeholder.shared[((threadIdx.z*36) + 31)])) + compute[31] = (compute[31] + (pad_temp.shared[(threadIdx.x + 145)]*placeholder.shared[((threadIdx.z*36) + 31)])) + compute[0] = (compute[0] + (pad_temp.shared[(threadIdx.x + 20)]*placeholder.shared[((threadIdx.z*36) + 5)])) + compute[1] = (compute[1] + (pad_temp.shared[(threadIdx.x + 38)]*placeholder.shared[((threadIdx.z*36) + 5)])) + compute[2] = (compute[2] + (pad_temp.shared[(threadIdx.x + 56)]*placeholder.shared[((threadIdx.z*36) + 5)])) + compute[3] = (compute[3] + (pad_temp.shared[(threadIdx.x + 74)]*placeholder.shared[((threadIdx.z*36) + 5)])) + compute[4] = (compute[4] + (pad_temp.shared[(threadIdx.x + 92)]*placeholder.shared[((threadIdx.z*36) + 5)])) + compute[5] = (compute[5] + (pad_temp.shared[(threadIdx.x + 110)]*placeholder.shared[((threadIdx.z*36) + 5)])) + compute[6] = (compute[6] + (pad_temp.shared[(threadIdx.x + 128)]*placeholder.shared[((threadIdx.z*36) + 5)])) + compute[7] = (compute[7] + (pad_temp.shared[(threadIdx.x + 146)]*placeholder.shared[((threadIdx.z*36) + 5)])) + compute[8] = (compute[8] + (pad_temp.shared[(threadIdx.x + 20)]*placeholder.shared[((threadIdx.z*36) + 14)])) + compute[9] = (compute[9] + (pad_temp.shared[(threadIdx.x + 38)]*placeholder.shared[((threadIdx.z*36) + 14)])) + compute[10] = (compute[10] + (pad_temp.shared[(threadIdx.x + 56)]*placeholder.shared[((threadIdx.z*36) + 14)])) + compute[11] = (compute[11] + (pad_temp.shared[(threadIdx.x + 74)]*placeholder.shared[((threadIdx.z*36) + 14)])) + compute[12] = (compute[12] + (pad_temp.shared[(threadIdx.x + 92)]*placeholder.shared[((threadIdx.z*36) + 14)])) + compute[13] = (compute[13] + (pad_temp.shared[(threadIdx.x + 110)]*placeholder.shared[((threadIdx.z*36) + 14)])) + compute[14] = (compute[14] + (pad_temp.shared[(threadIdx.x + 128)]*placeholder.shared[((threadIdx.z*36) + 14)])) + compute[15] = (compute[15] + (pad_temp.shared[(threadIdx.x + 146)]*placeholder.shared[((threadIdx.z*36) + 14)])) + compute[16] = (compute[16] + (pad_temp.shared[(threadIdx.x + 20)]*placeholder.shared[((threadIdx.z*36) + 23)])) + compute[17] = (compute[17] + (pad_temp.shared[(threadIdx.x + 38)]*placeholder.shared[((threadIdx.z*36) + 23)])) + compute[18] = (compute[18] + (pad_temp.shared[(threadIdx.x + 56)]*placeholder.shared[((threadIdx.z*36) + 23)])) + compute[19] = (compute[19] + (pad_temp.shared[(threadIdx.x + 74)]*placeholder.shared[((threadIdx.z*36) + 23)])) + compute[20] = (compute[20] + (pad_temp.shared[(threadIdx.x + 92)]*placeholder.shared[((threadIdx.z*36) + 23)])) + compute[21] = (compute[21] + (pad_temp.shared[(threadIdx.x + 110)]*placeholder.shared[((threadIdx.z*36) + 23)])) + compute[22] = (compute[22] + (pad_temp.shared[(threadIdx.x + 128)]*placeholder.shared[((threadIdx.z*36) + 23)])) + compute[23] = (compute[23] + (pad_temp.shared[(threadIdx.x + 146)]*placeholder.shared[((threadIdx.z*36) + 23)])) + compute[24] = (compute[24] + (pad_temp.shared[(threadIdx.x + 20)]*placeholder.shared[((threadIdx.z*36) + 32)])) + compute[25] = (compute[25] + (pad_temp.shared[(threadIdx.x + 38)]*placeholder.shared[((threadIdx.z*36) + 32)])) + compute[26] = (compute[26] + (pad_temp.shared[(threadIdx.x + 56)]*placeholder.shared[((threadIdx.z*36) + 32)])) + compute[27] = (compute[27] + (pad_temp.shared[(threadIdx.x + 74)]*placeholder.shared[((threadIdx.z*36) + 32)])) + compute[28] = (compute[28] + (pad_temp.shared[(threadIdx.x + 92)]*placeholder.shared[((threadIdx.z*36) + 32)])) + compute[29] = (compute[29] + (pad_temp.shared[(threadIdx.x + 110)]*placeholder.shared[((threadIdx.z*36) + 32)])) + compute[30] = (compute[30] + (pad_temp.shared[(threadIdx.x + 128)]*placeholder.shared[((threadIdx.z*36) + 32)])) + compute[31] = (compute[31] + (pad_temp.shared[(threadIdx.x + 146)]*placeholder.shared[((threadIdx.z*36) + 32)])) + compute[0] = (compute[0] + (pad_temp.shared[(threadIdx.x + 36)]*placeholder.shared[((threadIdx.z*36) + 6)])) + compute[1] = (compute[1] + (pad_temp.shared[(threadIdx.x + 54)]*placeholder.shared[((threadIdx.z*36) + 6)])) + compute[2] = (compute[2] + (pad_temp.shared[(threadIdx.x + 72)]*placeholder.shared[((threadIdx.z*36) + 6)])) + compute[3] = (compute[3] + (pad_temp.shared[(threadIdx.x + 90)]*placeholder.shared[((threadIdx.z*36) + 6)])) + compute[4] = (compute[4] + (pad_temp.shared[(threadIdx.x + 108)]*placeholder.shared[((threadIdx.z*36) + 6)])) + compute[5] = (compute[5] + (pad_temp.shared[(threadIdx.x + 126)]*placeholder.shared[((threadIdx.z*36) + 6)])) + compute[6] = (compute[6] + (pad_temp.shared[(threadIdx.x + 144)]*placeholder.shared[((threadIdx.z*36) + 6)])) + compute[7] = (compute[7] + (pad_temp.shared[(threadIdx.x + 162)]*placeholder.shared[((threadIdx.z*36) + 6)])) + compute[8] = (compute[8] + (pad_temp.shared[(threadIdx.x + 36)]*placeholder.shared[((threadIdx.z*36) + 15)])) + compute[9] = (compute[9] + (pad_temp.shared[(threadIdx.x + 54)]*placeholder.shared[((threadIdx.z*36) + 15)])) + compute[10] = (compute[10] + (pad_temp.shared[(threadIdx.x + 72)]*placeholder.shared[((threadIdx.z*36) + 15)])) + compute[11] = (compute[11] + (pad_temp.shared[(threadIdx.x + 90)]*placeholder.shared[((threadIdx.z*36) + 15)])) + compute[12] = (compute[12] + (pad_temp.shared[(threadIdx.x + 108)]*placeholder.shared[((threadIdx.z*36) + 15)])) + compute[13] = (compute[13] + (pad_temp.shared[(threadIdx.x + 126)]*placeholder.shared[((threadIdx.z*36) + 15)])) + compute[14] = (compute[14] + (pad_temp.shared[(threadIdx.x + 144)]*placeholder.shared[((threadIdx.z*36) + 15)])) + compute[15] = (compute[15] + (pad_temp.shared[(threadIdx.x + 162)]*placeholder.shared[((threadIdx.z*36) + 15)])) + compute[16] = (compute[16] + (pad_temp.shared[(threadIdx.x + 36)]*placeholder.shared[((threadIdx.z*36) + 24)])) + compute[17] = (compute[17] + (pad_temp.shared[(threadIdx.x + 54)]*placeholder.shared[((threadIdx.z*36) + 24)])) + compute[18] = (compute[18] + (pad_temp.shared[(threadIdx.x + 72)]*placeholder.shared[((threadIdx.z*36) + 24)])) + compute[19] = (compute[19] + (pad_temp.shared[(threadIdx.x + 90)]*placeholder.shared[((threadIdx.z*36) + 24)])) + compute[20] = (compute[20] + (pad_temp.shared[(threadIdx.x + 108)]*placeholder.shared[((threadIdx.z*36) + 24)])) + compute[21] = (compute[21] + (pad_temp.shared[(threadIdx.x + 126)]*placeholder.shared[((threadIdx.z*36) + 24)])) + compute[22] = (compute[22] + (pad_temp.shared[(threadIdx.x + 144)]*placeholder.shared[((threadIdx.z*36) + 24)])) + compute[23] = (compute[23] + (pad_temp.shared[(threadIdx.x + 162)]*placeholder.shared[((threadIdx.z*36) + 24)])) + compute[24] = (compute[24] + (pad_temp.shared[(threadIdx.x + 36)]*placeholder.shared[((threadIdx.z*36) + 33)])) + compute[25] = (compute[25] + (pad_temp.shared[(threadIdx.x + 54)]*placeholder.shared[((threadIdx.z*36) + 33)])) + compute[26] = (compute[26] + (pad_temp.shared[(threadIdx.x + 72)]*placeholder.shared[((threadIdx.z*36) + 33)])) + compute[27] = (compute[27] + (pad_temp.shared[(threadIdx.x + 90)]*placeholder.shared[((threadIdx.z*36) + 33)])) + compute[28] = (compute[28] + (pad_temp.shared[(threadIdx.x + 108)]*placeholder.shared[((threadIdx.z*36) + 33)])) + compute[29] = (compute[29] + (pad_temp.shared[(threadIdx.x + 126)]*placeholder.shared[((threadIdx.z*36) + 33)])) + compute[30] = (compute[30] + (pad_temp.shared[(threadIdx.x + 144)]*placeholder.shared[((threadIdx.z*36) + 33)])) + compute[31] = (compute[31] + (pad_temp.shared[(threadIdx.x + 162)]*placeholder.shared[((threadIdx.z*36) + 33)])) + compute[0] = (compute[0] + (pad_temp.shared[(threadIdx.x + 37)]*placeholder.shared[((threadIdx.z*36) + 7)])) + compute[1] = (compute[1] + (pad_temp.shared[(threadIdx.x + 55)]*placeholder.shared[((threadIdx.z*36) + 7)])) + compute[2] = (compute[2] + (pad_temp.shared[(threadIdx.x + 73)]*placeholder.shared[((threadIdx.z*36) + 7)])) + compute[3] = (compute[3] + (pad_temp.shared[(threadIdx.x + 91)]*placeholder.shared[((threadIdx.z*36) + 7)])) + compute[4] = (compute[4] + (pad_temp.shared[(threadIdx.x + 109)]*placeholder.shared[((threadIdx.z*36) + 7)])) + compute[5] = (compute[5] + (pad_temp.shared[(threadIdx.x + 127)]*placeholder.shared[((threadIdx.z*36) + 7)])) + compute[6] = (compute[6] + (pad_temp.shared[(threadIdx.x + 145)]*placeholder.shared[((threadIdx.z*36) + 7)])) + compute[7] = (compute[7] + (pad_temp.shared[(threadIdx.x + 163)]*placeholder.shared[((threadIdx.z*36) + 7)])) + compute[8] = (compute[8] + (pad_temp.shared[(threadIdx.x + 37)]*placeholder.shared[((threadIdx.z*36) + 16)])) + compute[9] = (compute[9] + (pad_temp.shared[(threadIdx.x + 55)]*placeholder.shared[((threadIdx.z*36) + 16)])) + compute[10] = (compute[10] + (pad_temp.shared[(threadIdx.x + 73)]*placeholder.shared[((threadIdx.z*36) + 16)])) + compute[11] = (compute[11] + (pad_temp.shared[(threadIdx.x + 91)]*placeholder.shared[((threadIdx.z*36) + 16)])) + compute[12] = (compute[12] + (pad_temp.shared[(threadIdx.x + 109)]*placeholder.shared[((threadIdx.z*36) + 16)])) + compute[13] = (compute[13] + (pad_temp.shared[(threadIdx.x + 127)]*placeholder.shared[((threadIdx.z*36) + 16)])) + compute[14] = (compute[14] + (pad_temp.shared[(threadIdx.x + 145)]*placeholder.shared[((threadIdx.z*36) + 16)])) + compute[15] = (compute[15] + (pad_temp.shared[(threadIdx.x + 163)]*placeholder.shared[((threadIdx.z*36) + 16)])) + compute[16] = (compute[16] + (pad_temp.shared[(threadIdx.x + 37)]*placeholder.shared[((threadIdx.z*36) + 25)])) + compute[17] = (compute[17] + (pad_temp.shared[(threadIdx.x + 55)]*placeholder.shared[((threadIdx.z*36) + 25)])) + compute[18] = (compute[18] + (pad_temp.shared[(threadIdx.x + 73)]*placeholder.shared[((threadIdx.z*36) + 25)])) + compute[19] = (compute[19] + (pad_temp.shared[(threadIdx.x + 91)]*placeholder.shared[((threadIdx.z*36) + 25)])) + compute[20] = (compute[20] + (pad_temp.shared[(threadIdx.x + 109)]*placeholder.shared[((threadIdx.z*36) + 25)])) + compute[21] = (compute[21] + (pad_temp.shared[(threadIdx.x + 127)]*placeholder.shared[((threadIdx.z*36) + 25)])) + compute[22] = (compute[22] + (pad_temp.shared[(threadIdx.x + 145)]*placeholder.shared[((threadIdx.z*36) + 25)])) + compute[23] = (compute[23] + (pad_temp.shared[(threadIdx.x + 163)]*placeholder.shared[((threadIdx.z*36) + 25)])) + compute[24] = (compute[24] + (pad_temp.shared[(threadIdx.x + 37)]*placeholder.shared[((threadIdx.z*36) + 34)])) + compute[25] = (compute[25] + (pad_temp.shared[(threadIdx.x + 55)]*placeholder.shared[((threadIdx.z*36) + 34)])) + compute[26] = (compute[26] + (pad_temp.shared[(threadIdx.x + 73)]*placeholder.shared[((threadIdx.z*36) + 34)])) + compute[27] = (compute[27] + (pad_temp.shared[(threadIdx.x + 91)]*placeholder.shared[((threadIdx.z*36) + 34)])) + compute[28] = (compute[28] + (pad_temp.shared[(threadIdx.x + 109)]*placeholder.shared[((threadIdx.z*36) + 34)])) + compute[29] = (compute[29] + (pad_temp.shared[(threadIdx.x + 127)]*placeholder.shared[((threadIdx.z*36) + 34)])) + compute[30] = (compute[30] + (pad_temp.shared[(threadIdx.x + 145)]*placeholder.shared[((threadIdx.z*36) + 34)])) + compute[31] = (compute[31] + (pad_temp.shared[(threadIdx.x + 163)]*placeholder.shared[((threadIdx.z*36) + 34)])) + compute[0] = (compute[0] + (pad_temp.shared[(threadIdx.x + 38)]*placeholder.shared[((threadIdx.z*36) + 8)])) + compute[1] = (compute[1] + (pad_temp.shared[(threadIdx.x + 56)]*placeholder.shared[((threadIdx.z*36) + 8)])) + compute[2] = (compute[2] + (pad_temp.shared[(threadIdx.x + 74)]*placeholder.shared[((threadIdx.z*36) + 8)])) + compute[3] = (compute[3] + (pad_temp.shared[(threadIdx.x + 92)]*placeholder.shared[((threadIdx.z*36) + 8)])) + compute[4] = (compute[4] + (pad_temp.shared[(threadIdx.x + 110)]*placeholder.shared[((threadIdx.z*36) + 8)])) + compute[5] = (compute[5] + (pad_temp.shared[(threadIdx.x + 128)]*placeholder.shared[((threadIdx.z*36) + 8)])) + compute[6] = (compute[6] + (pad_temp.shared[(threadIdx.x + 146)]*placeholder.shared[((threadIdx.z*36) + 8)])) + compute[7] = (compute[7] + (pad_temp.shared[(threadIdx.x + 164)]*placeholder.shared[((threadIdx.z*36) + 8)])) + compute[8] = (compute[8] + (pad_temp.shared[(threadIdx.x + 38)]*placeholder.shared[((threadIdx.z*36) + 17)])) + compute[9] = (compute[9] + (pad_temp.shared[(threadIdx.x + 56)]*placeholder.shared[((threadIdx.z*36) + 17)])) + compute[10] = (compute[10] + (pad_temp.shared[(threadIdx.x + 74)]*placeholder.shared[((threadIdx.z*36) + 17)])) + compute[11] = (compute[11] + (pad_temp.shared[(threadIdx.x + 92)]*placeholder.shared[((threadIdx.z*36) + 17)])) + compute[12] = (compute[12] + (pad_temp.shared[(threadIdx.x + 110)]*placeholder.shared[((threadIdx.z*36) + 17)])) + compute[13] = (compute[13] + (pad_temp.shared[(threadIdx.x + 128)]*placeholder.shared[((threadIdx.z*36) + 17)])) + compute[14] = (compute[14] + (pad_temp.shared[(threadIdx.x + 146)]*placeholder.shared[((threadIdx.z*36) + 17)])) + compute[15] = (compute[15] + (pad_temp.shared[(threadIdx.x + 164)]*placeholder.shared[((threadIdx.z*36) + 17)])) + compute[16] = (compute[16] + (pad_temp.shared[(threadIdx.x + 38)]*placeholder.shared[((threadIdx.z*36) + 26)])) + compute[17] = (compute[17] + (pad_temp.shared[(threadIdx.x + 56)]*placeholder.shared[((threadIdx.z*36) + 26)])) + compute[18] = (compute[18] + (pad_temp.shared[(threadIdx.x + 74)]*placeholder.shared[((threadIdx.z*36) + 26)])) + compute[19] = (compute[19] + (pad_temp.shared[(threadIdx.x + 92)]*placeholder.shared[((threadIdx.z*36) + 26)])) + compute[20] = (compute[20] + (pad_temp.shared[(threadIdx.x + 110)]*placeholder.shared[((threadIdx.z*36) + 26)])) + compute[21] = (compute[21] + (pad_temp.shared[(threadIdx.x + 128)]*placeholder.shared[((threadIdx.z*36) + 26)])) + compute[22] = (compute[22] + (pad_temp.shared[(threadIdx.x + 146)]*placeholder.shared[((threadIdx.z*36) + 26)])) + compute[23] = (compute[23] + (pad_temp.shared[(threadIdx.x + 164)]*placeholder.shared[((threadIdx.z*36) + 26)])) + compute[24] = (compute[24] + (pad_temp.shared[(threadIdx.x + 38)]*placeholder.shared[((threadIdx.z*36) + 35)])) + compute[25] = (compute[25] + (pad_temp.shared[(threadIdx.x + 56)]*placeholder.shared[((threadIdx.z*36) + 35)])) + compute[26] = (compute[26] + (pad_temp.shared[(threadIdx.x + 74)]*placeholder.shared[((threadIdx.z*36) + 35)])) + compute[27] = (compute[27] + (pad_temp.shared[(threadIdx.x + 92)]*placeholder.shared[((threadIdx.z*36) + 35)])) + compute[28] = (compute[28] + (pad_temp.shared[(threadIdx.x + 110)]*placeholder.shared[((threadIdx.z*36) + 35)])) + compute[29] = (compute[29] + (pad_temp.shared[(threadIdx.x + 128)]*placeholder.shared[((threadIdx.z*36) + 35)])) + compute[30] = (compute[30] + (pad_temp.shared[(threadIdx.x + 146)]*placeholder.shared[((threadIdx.z*36) + 35)])) + compute[31] = (compute[31] + (pad_temp.shared[(threadIdx.x + 164)]*placeholder.shared[((threadIdx.z*36) + 35)])) + } + } + tensor[(((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x)] = max(((compute[0]*placeholder[(threadIdx.z*4)]) + placeholder[(threadIdx.z*4)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 224)] = max(((compute[1]*placeholder[(threadIdx.z*4)]) + placeholder[(threadIdx.z*4)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 448)] = max(((compute[2]*placeholder[(threadIdx.z*4)]) + placeholder[(threadIdx.z*4)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 672)] = max(((compute[3]*placeholder[(threadIdx.z*4)]) + placeholder[(threadIdx.z*4)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 896)] = max(((compute[4]*placeholder[(threadIdx.z*4)]) + placeholder[(threadIdx.z*4)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 1120)] = max(((compute[5]*placeholder[(threadIdx.z*4)]) + placeholder[(threadIdx.z*4)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 1344)] = max(((compute[6]*placeholder[(threadIdx.z*4)]) + placeholder[(threadIdx.z*4)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 1568)] = max(((compute[7]*placeholder[(threadIdx.z*4)]) + placeholder[(threadIdx.z*4)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 50176)] = max(((compute[8]*placeholder[((threadIdx.z*4) + 1)]) + placeholder[((threadIdx.z*4) + 1)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 50400)] = max(((compute[9]*placeholder[((threadIdx.z*4) + 1)]) + placeholder[((threadIdx.z*4) + 1)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 50624)] = max(((compute[10]*placeholder[((threadIdx.z*4) + 1)]) + placeholder[((threadIdx.z*4) + 1)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 50848)] = max(((compute[11]*placeholder[((threadIdx.z*4) + 1)]) + placeholder[((threadIdx.z*4) + 1)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 51072)] = max(((compute[12]*placeholder[((threadIdx.z*4) + 1)]) + placeholder[((threadIdx.z*4) + 1)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 51296)] = max(((compute[13]*placeholder[((threadIdx.z*4) + 1)]) + placeholder[((threadIdx.z*4) + 1)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 51520)] = max(((compute[14]*placeholder[((threadIdx.z*4) + 1)]) + placeholder[((threadIdx.z*4) + 1)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 51744)] = max(((compute[15]*placeholder[((threadIdx.z*4) + 1)]) + placeholder[((threadIdx.z*4) + 1)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 100352)] = max(((compute[16]*placeholder[((threadIdx.z*4) + 2)]) + placeholder[((threadIdx.z*4) + 2)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 100576)] = max(((compute[17]*placeholder[((threadIdx.z*4) + 2)]) + placeholder[((threadIdx.z*4) + 2)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 100800)] = max(((compute[18]*placeholder[((threadIdx.z*4) + 2)]) + placeholder[((threadIdx.z*4) + 2)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 101024)] = max(((compute[19]*placeholder[((threadIdx.z*4) + 2)]) + placeholder[((threadIdx.z*4) + 2)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 101248)] = max(((compute[20]*placeholder[((threadIdx.z*4) + 2)]) + placeholder[((threadIdx.z*4) + 2)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 101472)] = max(((compute[21]*placeholder[((threadIdx.z*4) + 2)]) + placeholder[((threadIdx.z*4) + 2)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 101696)] = max(((compute[22]*placeholder[((threadIdx.z*4) + 2)]) + placeholder[((threadIdx.z*4) + 2)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 101920)] = max(((compute[23]*placeholder[((threadIdx.z*4) + 2)]) + placeholder[((threadIdx.z*4) + 2)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 150528)] = max(((compute[24]*placeholder[((threadIdx.z*4) + 3)]) + placeholder[((threadIdx.z*4) + 3)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 150752)] = max(((compute[25]*placeholder[((threadIdx.z*4) + 3)]) + placeholder[((threadIdx.z*4) + 3)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 150976)] = max(((compute[26]*placeholder[((threadIdx.z*4) + 3)]) + placeholder[((threadIdx.z*4) + 3)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 151200)] = max(((compute[27]*placeholder[((threadIdx.z*4) + 3)]) + placeholder[((threadIdx.z*4) + 3)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 151424)] = max(((compute[28]*placeholder[((threadIdx.z*4) + 3)]) + placeholder[((threadIdx.z*4) + 3)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 151648)] = max(((compute[29]*placeholder[((threadIdx.z*4) + 3)]) + placeholder[((threadIdx.z*4) + 3)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 151872)] = max(((compute[30]*placeholder[((threadIdx.z*4) + 3)]) + placeholder[((threadIdx.z*4) + 3)]), 0.000000f) + tensor[((((((blockIdx.y*112) + blockIdx.x) + (threadIdx.z*12544))*16) + threadIdx.x) + 152096)] = max(((compute[31]*placeholder[((threadIdx.z*4) + 3)]) + placeholder[((threadIdx.z*4) + 3)]), 0.000000f) +} +``` + +## 将 cuDNN 用于卷积层 + +将选项 "-libs=cudnn" 附加到 target 字符串,从而用 cuDNN 将卷积核替换为 cuDNN。 + +``` python +net, params = testing.create_workload(simple_net) +target = "cuda -libs=cudnn" # use cudnn for convolution +lib = relay.build_module.build(net, target, params=params) + +dev = tvm.device(target, 0) +data = np.random.uniform(-1, 1, size=data_shape).astype("float32") +module = runtime.GraphModule(lib["default"](dev)) +module.set_input("data", data) +module.run() +out_shape = (batch_size, out_channels, 224, 224) +out = module.get_output(0, tvm.nd.empty(out_shape)) +out_cudnn = out.numpy() +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +注意,若用 cuDNN,Relay 无法将卷积与其后面的层融合。因为层融合发生在 TVM internal representation(IR)级别。 Relay 将外部库视为黑盒,因此无法将它们与 TVM IR 融合。 + +下面的伪代码显示了 cuDNN 卷积 + bias add + batch norm + ReLU 变成了两个计算阶段,一个用于 cuDNN 调用,另一个用于其余操作。 + +``` python +// attr [y] storage_scope = "global" +allocate y[float32 * 802816] +produce y { + // attr [0] extern_scope = 0 + tvm_call_packed("tvm.contrib.cudnn.conv2d.forward", 1, 0, 1, 1, 1, 1, 1, 1, 1, tvm_stack_make_array(placeholder, tvm_stack_make_shape(1, 3, 224, 224), 0, 4, 0.000000f, 0), tvm_stack_make_array(placeholder, tvm_stack_make_shape(16, 3, 3, 3), 0, 4, 0.000000f, 0), tvm_stack_make_array(y, tvm_stack_make_shape(1, 16, 224, 224), 0, 4, 0.000000f, 0)) +} +produce tensor { + // attr [iter_var(blockIdx.x, , blockIdx.x)] thread_extent = 256 + // attr [iter_var(threadIdx.x, , threadIdx.x)] thread_extent = 512 + for (ax0.ax1.fused.ax2.fused.ax3.fused.outer, 0, 7) { + if (likely(((blockIdx.x*512) < ((802816 - (ax0.ax1.fused.ax2.fused.ax3.fused.outer*131072)) - threadIdx.x)))) { + tensor[(((((((blockIdx.x*512) + threadIdx.x) + (ax0.ax1.fused.ax2.fused.ax3.fused.outer*131072))/802816)*802816) + (((((((blockIdx.x*512) + threadIdx.x) + (ax0.ax1.fused.ax2.fused.ax3.fused.outer*131072))/224) % 224)*224) + ((((blockIdx.x*64) + threadIdx.x) + (ax0.ax1.fused.ax2.fused.ax3.fused.outer*32)) % 224))) + ((((((blockIdx.x*512) + threadIdx.x) + (ax0.ax1.fused.ax2.fused.ax3.fused.outer*131072))/50176) % 16)*50176))] = max(((y[(((((((blockIdx.x*512) + threadIdx.x) + (ax0.ax1.fused.ax2.fused.ax3.fused.outer*131072))/802816)*802816) + (((((((blockIdx.x*512) + threadIdx.x) + (ax0.ax1.fused.ax2.fused.ax3.fused.outer*131072))/224) % 224)*224) + ((((blockIdx.x*64) + threadIdx.x) + (ax0.ax1.fused.ax2.fused.ax3.fused.outer*32)) % 224))) + ((((((blockIdx.x*512) + threadIdx.x) + (ax0.ax1.fused.ax2.fused.ax3.fused.outer*131072))/50176) % 16)*50176))]*placeholder[(((((blockIdx.x*512) + threadIdx.x) + (ax0.ax1.fused.ax2.fused.ax3.fused.outer*131072))/50176) % 16)]) + placeholder[(((((blockIdx.x*512) + threadIdx.x) + (ax0.ax1.fused.ax2.fused.ax3.fused.outer*131072))/50176) % 16)]), 0.000000f) + } + } +} +``` + +## 验证结果 + +检查两次运行的结果是否匹配。 + +``` python +tvm.testing.assert_allclose(out_cuda, out_cudnn, rtol=1e-5) +``` + +## 结论 + +本教程介绍了 cuDNN 与 Relay 的使用,此外还支持 cuBLAS。若启用了 cuBLAS,它将在全连接层(relay.dense)内使用。若要用 cuBLAS,请将 target 字符串设置为 "cuda -libs=cublas"。也可以将 cuDNN 和 cuBLAS 与 "cuda -libs=cudnn,cublas" 一起使用。 + +对于 ROCm 后端,支持 MIOpen 和 rocBLAS。将 target 设置为 "rocm -libs=miopen,rocblas" 以启用它们。 + +使用外部库的注意事项: + +首先,使用外部库可能会限制 TVM 和 Relay 的使用。例如,MIOpen 目前只支持 NCHW 布局和 fp32 数据类型,因此不能在 TVM 中使用其他布局或数据类型。 + +其次,外部库限制了计算图编译期间算子融合的可能性,如上所示。TVM 和 Relay 旨在通过联合算子级别和计算图级别优化,在各种硬件上实现最佳性能。为了实现这个目标,应该继续为 TVM 和 Relay 开发更好的优化,同时在必要时使用外部库回退到现有实现。 + +[下载 Python 源代码:using_external_lib.py](https://tvm.apache.org/docs/_downloads/d8509b0a8e7db9031303c1a1f6fd1e70/using_external_lib.py) + +[下载 Jupyter Notebook:using_external_lib.ipynb](https://tvm.apache.org/docs/_downloads/edc9d28c4fbc249e2e7b78002af63b84/using_external_lib.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/relay/03-pipeline.md b/versioned_docs/version-0.12.0/how_to/relay/03-pipeline.md new file mode 100644 index 00000000..97015030 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/relay/03-pipeline.md @@ -0,0 +1,283 @@ +--- +title: 在 Relay 中使用 Pipeline Executor +--- + +# 在 Relay 中使用 Pipeline Executor + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/work_with_relay/using_pipeline_executor.html#sphx-glr-download-how-to-work-with-relay-using-pipeline-executor-py) 下载完整的示例代码 +::: + +**作者**:[Hua Jiang](https://github.com/huajsj) + +本教程介绍如何将「Pipeline Executor」与 Relay 配合使用。 + +``` python +import tvm +from tvm import te +import numpy as np +from tvm.contrib import graph_executor as runtime +from tvm.relay.op.contrib.cutlass import partition_for_cutlass +from tvm import relay +from tvm.relay import testing +import tvm.testing +from tvm.contrib.cutlass import ( + has_cutlass, + num_cutlass_partitions, + finalize_modules, + finalize_modules_vm, +) + +img_size = 8 +``` + +## 创建一个简单的网络,这个网络也可以是一个预训练的模型。 + +创建一个由 convolution、batch normalization、dense 和 ReLU activation 组成的网络用于演示。 + +``` python +def get_network(): + out_channels = 16 + batch_size = 1 + data = relay.var("data", relay.TensorType((batch_size, 3, img_size, img_size), "float16")) + dense_weight = relay.var( + "dweight", relay.TensorType((batch_size, 16 * img_size * img_size), "float16") + ) + weight = relay.var("weight") + second_weight = relay.var("second_weight") + bn_gamma = relay.var("bn_gamma") + bn_beta = relay.var("bn_beta") + bn_mmean = relay.var("bn_mean") + bn_mvar = relay.var("bn_var") + simple_net = relay.nn.conv2d( + data=data, weight=weight, kernel_size=(3, 3), channels=out_channels, padding=(1, 1) + ) + simple_net = relay.nn.batch_norm(simple_net, bn_gamma, bn_beta, bn_mmean, bn_mvar)[0] + simple_net = relay.nn.relu(simple_net) + simple_net = relay.nn.batch_flatten(simple_net) + simple_net = relay.nn.dense(simple_net, dense_weight) + simple_net = relay.Function(relay.analysis.free_vars(simple_net), simple_net) + data_shape = (batch_size, 3, img_size, img_size) + net, params = testing.create_workload(simple_net) + return net, params, data_shape + +net, params, data_shape = get_network() +``` + +## 将网络拆分成两个子图。 + +这个来自单元测试的名为「graph_split」的函数只是一个例子。用户可以创建自定义逻辑来拆分计算图。 + +``` python +import inspect +import os + +tutorial_dir = os.path.dirname(inspect.getfile(lambda: None)) +os.sys.path.append(os.path.join(tutorial_dir, "../../../tests/python/relay")) +from test_pipeline_executor import graph_split +``` + +将网络拆分成两个子图。 + +``` python +split_config = [{"op_name": "nn.relu", "op_index": 0}] +subgraphs = graph_split(net["main"], split_config, params) +``` + +生成的子图如下所示。 + +``` bash +""" +#subgraphs[0]) + + def @main(%data: Tensor[(1, 3, img_size, img_size), float16]) { + %0 = nn.conv2d(%data, meta[relay.Constant][0] /* ty=Tensor[(16, 3, 3, 3), float16] */, padding=[1, 1, 1, 1], channels=16, kernel_size=[3, 3]) /* ty=Tensor[(1, 16, img_size, img_size), float16] */; + %1 = nn.batch_norm(%0, meta[relay.Constant][1] /* ty=Tensor[(16), float16] */, meta[relay.Constant][2] /* ty=Tensor[(16), float16]*/, meta[relay.Constant][3] /* ty=Tensor[(16), float16] */, meta[relay.Constant][4] /* ty=Tensor[(16), float16] */) /* ty=(Tensor[(1,16, img_size, img_size), float16], Tensor[(16), float16], Tensor[(16), float16]) */; + %2 = %1.0; + nn.relu(%2) /* ty=Tensor[(1, 16, img_size, img_size), float16] */ + } + +#subgraphs[1] + + def @main(%data_n_0: Tensor[(1, 16, 8, 8), float16] /* ty=Tensor[(1, 16, 8, 8), float16] */) { + %0 = nn.batch_flatten(%data_n_0) /* ty=Tensor[(1, 1024), float16] */; + nn.dense(%0, meta[relay.Constant][0] /* ty=Tensor[(1, 1024), float16] */, units=None) /* ty=Tensor[(1, 1), float16] */ + } + +""" +``` + +## 用 cutlass target 构建子图。 + +``` python +cutlass = tvm.target.Target( + { + "kind": "cutlass", + "sm": int(tvm.target.Target("cuda").arch.split("_")[1]), + "use_3xtf32": True, + "split_k_slices": [1], + "profile_all_alignments": False, + "find_first_valid": True, + "use_multiprocessing": True, + "use_fast_math": False, + "tmp_dir": "./tmp", + }, + host=tvm.target.Target("llvm"), +) + +def cutlass_build(mod, target, params=None, target_host=None, mod_name="default"): + target = [target, cutlass] + lib = relay.build_module.build( + mod, target=target, params=params, target_host=target_host, mod_name=mod_name + ) + return lib +``` + +## 使用 pipeline executor 在 pipeline 中运行两个子图。 + +在 cmake 中将 `USE_PIPELINE_EXECUTOR` 和 `USE_CUTLASS` 设置为 ON。 + +``` python +from tvm.contrib import graph_executor, pipeline_executor, pipeline_executor_build +``` + +创建子图 pipeline 配置。将子图模块与 target 关联起来。使用 CUTLASS BYOC 构建第二个子图模块。 + +``` python +mod0, mod1 = subgraphs[0], subgraphs[1] +# 将 cutlass 作为 codegen。 +mod1 = partition_for_cutlass(mod1) +``` + +获取 pipeline executor 配置对象。 + +``` python +pipe_config = pipeline_executor_build.PipelineConfig() +``` + +设置子图模块的编译 target。 + +``` python +pipe_config[mod0].target = "llvm" +pipe_config[mod0].dev = tvm.cpu(0) +``` + +将第二个子图模块的编译 target 设置为 cuda。 + +``` python +pipe_config[mod1].target = "cuda" +pipe_config[mod1].dev = tvm.device("cuda", 0) +pipe_config[mod1].build_func = cutlass_build +pipe_config[mod1].export_cc = "nvcc" +# 通过连接子图模块创建 pipeline。 +# 全局输入将被转发到第一个名为 mod0 的模块的输入接口 +pipe_config["input"]["data"].connect(pipe_config[mod0]["input"]["data"]) +# mod0 的第一个输出会转发到 mod1 的输入接口 +pipe_config[mod0]["output"][0].connect(pipe_config[mod1]["input"]["data_n_0"]) +# mod1 的第一个输出将是第一个全局输出。 +pipe_config[mod1]["output"][0].connect(pipe_config["output"][0]) +``` + +pipeline 配置如下: + +``` bash +""" +print(pipe_config) + Inputs + |data: mod0:data + + output + |output(0) : mod1.output(0) + + connections + |mod0.output(0)-> mod1.data_n_0 +""" +``` + +## 构建 pipeline executor。 + +``` python +with tvm.transform.PassContext(opt_level=3): + pipeline_mod_factory = pipeline_executor_build.build(pipe_config) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:267: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +将参数配置导出到一个文件中。 + +``` python +directory_path = tvm.contrib.utils.tempdir().temp_dir +os.makedirs(directory_path, exist_ok=True) +config_file_name = pipeline_mod_factory.export_library(directory_path) +``` + +## 使用 load 函数创建和初始化 PipelineModule。 + +``` python +pipeline_module = pipeline_executor.PipelineModule.load_library(config_file_name) +``` + +## 运行 pipeline executor。 + +分配输入数据。 + +``` python +data = np.random.uniform(-1, 1, size=data_shape).astype("float16") +pipeline_module.set_input("data", tvm.nd.array(data)) +``` + +以 pipeline 模式运行两个子图,异步或同步获取输出。以下示例为同步获取输出。 + +``` python +pipeline_module.run() +outputs = pipeline_module.get_output() +``` + +## 使用 graph_executor 进行验证。 + +用 graph_executor 依次运行这两个子图,得到输出。 + +``` python +target = "llvm" +dev0 = tvm.device(target, 0) +lib0 = relay.build_module.build(mod0, target, params=params) +module0 = runtime.GraphModule(lib0["default"](dev0)) +cuda = tvm.target.Target("cuda", host=tvm.target.Target("llvm")) +lib1 = relay.build_module.build(mod1, [cuda, cutlass], params=params) +lib1 = finalize_modules(lib1, "compile.so", "./tmp") + +dev1 = tvm.device("cuda", 0) + +module1 = runtime.GraphModule(lib1["default"](dev1)) + +module0.set_input("data", data) +module0.run() +out_shape = (1, 16, img_size, img_size) +out = module0.get_output(0, tvm.nd.empty(out_shape, "float16")) +module1.set_input("data_n_0", out) +module1.run() +out_shape = (1, 1) +out = module1.get_output(0, tvm.nd.empty(out_shape, "float16")) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:267: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +验证结果。 + +``` python +tvm.testing.assert_allclose(outputs[0].numpy(), out.numpy()) +``` + +[下载 Python 源代码:using_pipeline_executor.py](https://tvm.apache.org/docs/_downloads/29c30a5341c6aa08601b51791150fa4b/using_pipeline_executor.py) + +[下载 Jupyter Notebook:using_pipeline_executor.ipynb](https://tvm.apache.org/docs/_downloads/f407f66fb8174d0d4ec37407af1128d6/using_pipeline_executor.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/relay/04-relay_visualizer.md b/versioned_docs/version-0.12.0/how_to/relay/04-relay_visualizer.md new file mode 100644 index 00000000..6c6875a0 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/relay/04-relay_visualizer.md @@ -0,0 +1,200 @@ +--- +title: 使用 Relay Visualizer 可视化 Relay +--- + +# 使用 Relay Visualizer 可视化 Relay + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/work_with_relay/using_relay_viz.html#sphx-glr-download-how-to-work-with-relay-using-relay-viz-py) 下载完整的示例代码 +::: + +**作者**:[Chi-Wei Wang](https://github.com/chiwwang) + +Relay IR 模块可以包含很多操作。通常单个操作很容易理解,但放在一起可能会使计算图难以阅读。随着优化 pass 发挥作用,情况可能会变得更糟。 + +该实用程序定义了一组接口(包括解析器、绘图器/渲染器、计算图、节点和边)将 IR 模块可视化为节点和边,且提供了默认解析器。用户可以用自己的渲染器对计算图进行渲染。 + +这里用渲染器来渲染文本形式的计算图,它是一个轻量级、类似 AST 可视化工具(灵感来自 [clang ast-dump](https://clang.llvm.org/docs/IntroductionToTheClangAST.html))。以下将介绍如何通过接口类来实现自定义的解析器和渲染器。 + +更多细节参考 `tvm.contrib.relay_viz`。 + +``` python +from typing import ( + Dict, + Union, + Tuple, + List, +) +import tvm +from tvm import relay +from tvm.contrib import relay_viz +from tvm.contrib.relay_viz.interface import ( + VizEdge, + VizNode, + VizParser, +) +from tvm.contrib.relay_viz.terminal import ( + TermGraph, + TermPlotter, + TermVizParser, +) +``` + +## 定义具有多个 GlobalVar 的 Relay IR 模块 + +构建一个包含多个 `GlobalVar` 的 Relay IR 模块示例。定义一个 `add` 函数,并在 main 函数中调用。 + +``` python +data = relay.var("data") +bias = relay.var("bias") +add_op = relay.add(data, bias) +add_func = relay.Function([data, bias], add_op) +add_gvar = relay.GlobalVar("AddFunc") + +input0 = relay.var("input0") +input1 = relay.var("input1") +input2 = relay.var("input2") +add_01 = relay.Call(add_gvar, [input0, input1]) +add_012 = relay.Call(add_gvar, [input2, add_01]) +main_func = relay.Function([input0, input1, input2], add_012) +main_gvar = relay.GlobalVar("main") + +mod = tvm.IRModule({main_gvar: main_func, add_gvar: add_func}) +``` + +## 在终端上使用 Relay Visualizer 渲染图形 + +终端可以用类似于 clang AST-dump 的文本显示 Relay IR 模块,可以看到 `AddFunc` 函数在 `main` 函数中调用了两次。 + +``` python +viz = relay_viz.RelayVisualizer(mod) +viz.render() +``` + +输出结果: + +``` bash +@main([Var(input0), Var(input1), Var(input2)]) +`--Call + |--GlobalVar AddFunc + |--Var(Input) name_hint: input2 + `--Call + |--GlobalVar AddFunc + |--Var(Input) name_hint: input0 + `--Var(Input) name_hint: input1 +@AddFunc([Var(data), Var(bias)]) +`--Call + |--add + |--Var(Input) name_hint: data + `--Var(Input) name_hint: bias +``` + +## 为 Relay 类型自定义解析器 + +有时想要强调感兴趣的信息,或者针对特定用途对事物进行不同的解析,需要遵守接口来定制解析器,下面演示如何自定义 `relay.var` 的解析器,需要实现抽象接口 `tvm.contrib.relay_viz.interface.VizParser`。 + +``` python +class YourAwesomeParser(VizParser): + def __init__(self): + self._delegate = TermVizParser() + + def get_node_edges( + self, + node: relay.Expr, + relay_param: Dict[str, tvm.runtime.NDArray], + node_to_id: Dict[relay.Expr, str], + ) -> Tuple[Union[VizNode, None], List[VizEdge]]: + + if isinstance(node, relay.Var): + node = VizNode(node_to_id[node], "AwesomeVar", f"name_hint {node.name_hint}") + # 没有引入边缘。所以返回一个空列表 + return node, [] + + # 将其他类型委托给其他解析器。 + return self._delegate.get_node_edges(node, relay_param, node_to_id) +``` + +将解析器和感兴趣的渲染器传递给 visualizer,这里只用终端渲染器。 + +``` python +viz = relay_viz.RelayVisualizer(mod, {}, TermPlotter(), YourAwesomeParser()) +viz.render() +``` + +输出结果: + +``` bash +@main([Var(input0), Var(input1), Var(input2)]) +`--Call + |--GlobalVar AddFunc + |--AwesomeVar name_hint input2 + `--Call + |--GlobalVar AddFunc + |--AwesomeVar name_hint input0 + `--AwesomeVar name_hint input1 +@AddFunc([Var(data), Var(bias)]) +`--Call + |--add + |--AwesomeVar name_hint data + `--AwesomeVar name_hint bias +``` + +## 围绕计算图和绘图器进行定制 + +除了解析器,还可以通过实现抽象类 `tvm.contrib.relay_viz.interface.VizGraph` 和 `tvm.contrib.relay_viz.interface.Plotter` 来自定义计算图和渲染器。下面重写了 `terminal.py` 中定义的 `TermGraph`。我们添加了一个 hook 在 `AwesomeVar` 上方复制,并让 `TermPlotter` 使用新类。 + +``` python +class AwesomeGraph(TermGraph): + def node(self, viz_node): + # 先添加节点 + super().node(viz_node) + # 如果是 AwesomeVar,复制它。 + if viz_node.type_name == "AwesomeVar": + duplicated_id = f"duplicated_{viz_node.identity}" + duplicated_type = "double AwesomeVar" + super().node(VizNode(duplicated_id, duplicated_type, "")) + # 将复制的 var 连接到原始的 + super().edge(VizEdge(duplicated_id, viz_node.identity)) + +# 使用 `AwesomeGraph` 覆盖 TermPlotter +class AwesomePlotter(TermPlotter): + def create_graph(self, name): + self._name_to_graph[name] = AwesomeGraph(name) + return self._name_to_graph[name] + +viz = relay_viz.RelayVisualizer(mod, {}, AwesomePlotter(), YourAwesomeParser()) +viz.render() +``` + +输出结果: + +``` bash +@main([Var(input0), Var(input1), Var(input2)]) +`--Call + |--GlobalVar AddFunc + |--AwesomeVar name_hint input2 + | `--double AwesomeVar + `--Call + |--GlobalVar AddFunc + |--AwesomeVar name_hint input0 + | `--double AwesomeVar + `--AwesomeVar name_hint input1 + `--double AwesomeVar +@AddFunc([Var(data), Var(bias)]) +`--Call + |--add + |--AwesomeVar name_hint data + | `--double AwesomeVar + `--AwesomeVar name_hint bias + `--double AwesomeVar +``` + +## 总结 + +本教程演示了 Relay Visualizer 的使用和自定义。 `tvm.contrib.relay_viz.RelayVisualizer` 类由 `interface.py` 中定义的接口组成。 + +目的是快速查看,然后修复迭代,构造函数参数尽可能简单,而自定义可以通过一组接口类进行。 + +[下载 Python 源代码:using_relay_viz.py](https://tvm.apache.org/docs/_downloads/cb089f2129f9829a01cc54eb81528811/using_relay_viz.py) + +[下载 Jupyter Notebook:using_relay_viz.ipynb](https://tvm.apache.org/docs/_downloads/b954238c1884e83b45d2ae543d824f03/using_relay_viz.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/relay/_category_.json b/versioned_docs/version-0.12.0/how_to/relay/_category_.json new file mode 100644 index 00000000..0e005246 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/relay/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 130 +} diff --git a/versioned_docs/version-0.12.0/how_to/relay/index.md b/versioned_docs/version-0.12.0/how_to/relay/index.md new file mode 100644 index 00000000..d616454a --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/relay/index.md @@ -0,0 +1,8 @@ +--- +title: 使用 Relay +--- + +* [构建图卷积网络](build_network) +* [在 Relay 中使用外部库](extenal_lib) +* [在 Relay 中使用 Pipeline Executor](pipeline) +* [使用 Relay Visualizer 可视化 Relay](relay_visualizer) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/te_schedules/01-primitive.md b/versioned_docs/version-0.12.0/how_to/te_schedules/01-primitive.md new file mode 100644 index 00000000..73793365 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/te_schedules/01-primitive.md @@ -0,0 +1,427 @@ +--- +title: TVM 中的 Schedule 原语 +--- + +# TVM 中的 Schedule 原语 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/work_with_schedules/schedule_primitives.html#sphx-glr-download-how-to-work-with-schedules-schedule-primitives-py) 下载完整的示例代码 +::: + +**作者**:[Ziheng Jiang](https://github.com/ZihengJiang) + +TVM 是一种用于高效构建内核的领域特定语言。 + +本教程展示了如何通过 TVM 提供的各种原语来调度计算。 + +``` python +from __future__ import absolute_import, print_function + +import tvm +from tvm import te +import numpy as np +``` + +计算相同结果的方法众多,然而,不同的方法会导致局部性和性能各异,因此 TVM 要求用户借助 **Schedule** 执行计算。 + +**Schedule** 是一组计算转换,可用于转换程序中的循环计算。 + +``` python +# 声明变量,供之后使用 +n = te.var("n") +m = te.var("m") +``` + +Schedule 可由算子列表创建,它默认以行优先的方式串行计算张量。 + +``` python +# 声明一个矩阵元素乘法 +A = te.placeholder((m, n), name="A") +B = te.placeholder((m, n), name="B") +C = te.compute((m, n), lambda i, j: A[i, j] * B[i, j], name="C") + +s = te.create_schedule([C.op]) +# lower 会将计算从定义转换为实际可调用的函数。 +# 使用参数 `simple_mode=True` 会返回一个可读的类 C 的语句,这里用它来打印 schedule 结果。 +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [(stride: int32*m: int32)], [], type="auto"), + B: Buffer(B_2: Pointer(float32), float32, [(stride_1: int32*m)], [], type="auto"), + C: Buffer(C_2: Pointer(float32), float32, [(stride_2: int32*m)], [], type="auto")} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [m, n: int32], [stride, stride_3: int32], type="auto"), B_1: B_3: Buffer(B_2, float32, [m, n], [stride_1, stride_4: int32], type="auto"), C_1: C_3: Buffer(C_2, float32, [m, n], [stride_2, stride_5: int32], type="auto")} { + for (i: int32, 0, m) { + for (j: int32, 0, n) { + C[((i*stride_2) + (j*stride_5))] = (A[((i*stride) + (j*stride_3))]*B[((i*stride_1) + (j*stride_4))]) + } + } +} +``` + +一个 Schedule 由多个 Stage 组成,一个 **Stage** 代表一个操作的 schedule。每个 stage 的调度都有多种方法: + +## split + +`split` 可根据 `factor` 将指定 axis 拆分为两个 axis。 + +``` python +A = te.placeholder((m,), name="A") +B = te.compute((m,), lambda i: A[i] * 2, name="B") + +s = te.create_schedule(B.op) +xo, xi = s[B].split(B.op.axis[0], factor=32) +print(tvm.lower(s, [A, B], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [(stride: int32*m: int32)], [], type="auto"), + B: Buffer(B_2: Pointer(float32), float32, [(stride_1: int32*m)], [], type="auto")} + buffer_map = {A_1: A, B_1: B} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [m], [stride], type="auto"), B_1: B_3: Buffer(B_2, float32, [m], [stride_1], type="auto")} { + for (i.outer: int32, 0, floordiv((m + 31), 32)) { + for (i.inner: int32, 0, 32) { + if @tir.likely((((i.outer*32) + i.inner) < m), dtype=bool) { + let cse_var_1: int32 = ((i.outer*32) + i.inner) + B[(cse_var_1*stride_1)] = (A[(cse_var_1*stride)]*2f32) + } + } + } +} +``` + +也可用 `nparts` 来拆分 axis,它拆分 axis 的方式与 `factor` 相反。 + +``` python +A = te.placeholder((m,), name="A") +B = te.compute((m,), lambda i: A[i], name="B") + +s = te.create_schedule(B.op) +bx, tx = s[B].split(B.op.axis[0], nparts=32) +print(tvm.lower(s, [A, B], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [(stride: int32*m: int32)], [], type="auto"), + B: Buffer(B_2: Pointer(float32), float32, [(stride_1: int32*m)], [], type="auto")} + buffer_map = {A_1: A, B_1: B} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [m], [stride], type="auto"), B_1: B_3: Buffer(B_2, float32, [m], [stride_1], type="auto")} { + for (i.outer: int32, 0, 32) { + for (i.inner: int32, 0, floordiv((m + 31), 32)) { + if @tir.likely(((i.inner + (i.outer*floordiv((m + 31), 32))) < m), dtype=bool) { + B[((i.inner + (i.outer*floordiv((m + 31), 32)))*stride_1)] = A[((i.inner + (i.outer*floordiv((m + 31), 32)))*stride)] + } + } + } +} +``` + +## tile + +`tile` 可在两个 axis 上逐块执行计算。 + +``` python +A = te.placeholder((m, n), name="A") +B = te.compute((m, n), lambda i, j: A[i, j], name="B") + +s = te.create_schedule(B.op) +xo, yo, xi, yi = s[B].tile(B.op.axis[0], B.op.axis[1], x_factor=10, y_factor=5) +print(tvm.lower(s, [A, B], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [(stride: int32*m: int32)], [], type="auto"), + B: Buffer(B_2: Pointer(float32), float32, [(stride_1: int32*m)], [], type="auto")} + buffer_map = {A_1: A, B_1: B} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [m, n: int32], [stride, stride_2: int32], type="auto"), B_1: B_3: Buffer(B_2, float32, [m, n], [stride_1, stride_3: int32], type="auto")} { + for (i.outer: int32, 0, floordiv((m + 9), 10)) { + for (j.outer: int32, 0, floordiv((n + 4), 5)) { + for (i.inner: int32, 0, 10) { + if @tir.likely((((i.outer*10) + i.inner) < m), dtype=bool) { + for (j.inner: int32, 0, 5) { + if @tir.likely((((j.outer*5) + j.inner) < n), dtype=bool) { + let cse_var_2: int32 = ((j.outer*5) + j.inner) + let cse_var_1: int32 = ((i.outer*10) + i.inner) + B[((cse_var_1*stride_1) + (cse_var_2*stride_3))] = A[((cse_var_1*stride) + (cse_var_2*stride_2))] + } + } + } + } + } + } +} +``` + +## fuse + +`fuse` 可将一个计算的两个连续 axis 融合。 + +``` python +A = te.placeholder((m, n), name="A") +B = te.compute((m, n), lambda i, j: A[i, j], name="B") + +s = te.create_schedule(B.op) +# 首先调用 tile 平铺到四个 axis: (i.outer, j.outer, i.inner, j.inner) +xo, yo, xi, yi = s[B].tile(B.op.axis[0], B.op.axis[1], x_factor=10, y_factor=5) +# 然后将 (i.inner, j.inner) 融合成一个轴: (i.inner.j.inner.fused) +fused = s[B].fuse(xi, yi) +print(tvm.lower(s, [A, B], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [(stride: int32*m: int32)], [], type="auto"), + B: Buffer(B_2: Pointer(float32), float32, [(stride_1: int32*m)], [], type="auto")} + buffer_map = {A_1: A, B_1: B} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [m, n: int32], [stride, stride_2: int32], type="auto"), B_1: B_3: Buffer(B_2, float32, [m, n], [stride_1, stride_3: int32], type="auto")} { + for (i.outer: int32, 0, floordiv((m + 9), 10)) { + for (j.outer: int32, 0, floordiv((n + 4), 5)) { + for (i.inner.j.inner.fused: int32, 0, 50) { + if @tir.likely((((i.outer*10) + floordiv(i.inner.j.inner.fused, 5)) < m), dtype=bool) { + if @tir.likely((((j.outer*5) + floormod(i.inner.j.inner.fused, 5)) < n), dtype=bool) { + let cse_var_2: int32 = ((j.outer*5) + floormod(i.inner.j.inner.fused, 5)) + let cse_var_1: int32 = ((i.outer*10) + floordiv(i.inner.j.inner.fused, 5)) + B[((cse_var_1*stride_1) + (cse_var_2*stride_3))] = A[((cse_var_1*stride) + (cse_var_2*stride_2))] + } + } + } + } + } +} +``` + +## reorder + +`reorder` 可按指定的顺序对 axis 重新排序。 + +``` python +A = te.placeholder((m, n), name="A") +B = te.compute((m, n), lambda i, j: A[i, j], name="B") + +s = te.create_schedule(B.op) +# 首先调用 tile 平铺到四个轴: (i.outer, j.outer, i.inner, j.inner) +xo, yo, xi, yi = s[B].tile(B.op.axis[0], B.op.axis[1], x_factor=10, y_factor=5) +# 然后将 axis 重新排序:(i.inner,j.outer,i.outer,j.inner) +s[B].reorder(xi, yo, xo, yi) +print(tvm.lower(s, [A, B], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [(stride: int32*m: int32)], [], type="auto"), + B: Buffer(B_2: Pointer(float32), float32, [(stride_1: int32*m)], [], type="auto")} + buffer_map = {A_1: A, B_1: B} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [m, n: int32], [stride, stride_2: int32], type="auto"), B_1: B_3: Buffer(B_2, float32, [m, n], [stride_1, stride_3: int32], type="auto")} { + for (i.inner: int32, 0, 10) { + for (j.outer: int32, 0, floordiv((n + 4), 5)) { + for (i.outer: int32, 0, floordiv((m + 9), 10)) { + if @tir.likely((((i.outer*10) + i.inner) < m), dtype=bool) { + for (j.inner: int32, 0, 5) { + if @tir.likely((((j.outer*5) + j.inner) < n), dtype=bool) { + let cse_var_2: int32 = ((j.outer*5) + j.inner) + let cse_var_1: int32 = ((i.outer*10) + i.inner) + B[((cse_var_1*stride_1) + (cse_var_2*stride_3))] = A[((cse_var_1*stride) + (cse_var_2*stride_2))] + } + } + } + } + } + } +} +``` + +## bind + +`bind` 可将指定 axis 与线程 axis 绑定,常用于 GPU 编程。 + +``` python +A = te.placeholder((n,), name="A") +B = te.compute(A.shape, lambda i: A[i] * 2, name="B") + +s = te.create_schedule(B.op) +bx, tx = s[B].split(B.op.axis[0], factor=64) +s[B].bind(bx, te.thread_axis("blockIdx.x")) +s[B].bind(tx, te.thread_axis("threadIdx.x")) +print(tvm.lower(s, [A, B], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [(stride: int32*n: int32)], [], type="auto"), + B: Buffer(B_2: Pointer(float32), float32, [(stride_1: int32*n)], [], type="auto")} + buffer_map = {A_1: A, B_1: B} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [n], [stride], type="auto"), B_1: B_3: Buffer(B_2, float32, [n], [stride_1], type="auto")} { + attr [IterVar(blockIdx.x: int32, (nullptr), "ThreadIndex", "blockIdx.x")] "thread_extent" = floordiv((n + 63), 64); + attr [IterVar(threadIdx.x: int32, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 64; + if @tir.likely((((blockIdx.x*64) + threadIdx.x) < n), dtype=bool) { + B[(((blockIdx.x*64) + threadIdx.x)*stride_1)] = (A[(((blockIdx.x*64) + threadIdx.x)*stride)]*2f32) + } +} +``` + +## compute_at + +对于包含多个算子的 schedule,TVM 默认会分别计算 root 处的张量。 + +``` python +A = te.placeholder((m,), name="A") +B = te.compute((m,), lambda i: A[i] + 1, name="B") +C = te.compute((m,), lambda i: B[i] * 2, name="C") + +s = te.create_schedule(C.op) +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [(stride: int32*m: int32)], [], type="auto"), + B: Buffer(B_2: Pointer(float32), float32, [(stride_1: int32*m)], [], type="auto"), + C: Buffer(C_2: Pointer(float32), float32, [(stride_2: int32*m)], [], type="auto")} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [m], [stride], type="auto"), B_1: B_3: Buffer(B_2, float32, [m], [stride_1], type="auto"), C_1: C_3: Buffer(C_2, float32, [m], [stride_2], type="auto")} { + for (i: int32, 0, m) { + B[(i*stride_1)] = (A[(i*stride)] + 1f32) + } + for (i_1: int32, 0, m) { + C[(i_1*stride_2)] = (B[(i_1*stride_1)]*2f32) + } +} +``` + +`compute_at` 可将 B 的计算移动到 C 计算的首个 axis 中。 + +``` python +A = te.placeholder((m,), name="A") +B = te.compute((m,), lambda i: A[i] + 1, name="B") +C = te.compute((m,), lambda i: B[i] * 2, name="C") + +s = te.create_schedule(C.op) +s[B].compute_at(s[C], C.op.axis[0]) +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [(stride: int32*m: int32)], [], type="auto"), + B: Buffer(B_2: Pointer(float32), float32, [(stride_1: int32*m)], [], type="auto"), + C: Buffer(C_2: Pointer(float32), float32, [(stride_2: int32*m)], [], type="auto")} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [m], [stride], type="auto"), B_1: B_3: Buffer(B_2, float32, [m], [stride_1], type="auto"), C_1: C_3: Buffer(C_2, float32, [m], [stride_2], type="auto")} { + for (i: int32, 0, m) { + B[(i*stride_1)] = (A[(i*stride)] + 1f32) + C[(i*stride_2)] = (B[(i*stride_1)]*2f32) + } +} +``` + +## compute_inline + +`compute_inline` 可将 stage 标记为 inline,然后扩展计算体,并将其插入到需要张量的地址。 + +``` python +A = te.placeholder((m,), name="A") +B = te.compute((m,), lambda i: A[i] + 1, name="B") +C = te.compute((m,), lambda i: B[i] * 2, name="C") + +s = te.create_schedule(C.op) +s[B].compute_inline() +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [(stride: int32*m: int32)], [], type="auto"), + B: Buffer(B_2: Pointer(float32), float32, [(stride_1: int32*m)], [], type="auto"), + C: Buffer(C_2: Pointer(float32), float32, [(stride_2: int32*m)], [], type="auto")} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [m], [stride], type="auto"), B_1: B_3: Buffer(B_2, float32, [m], [stride_1], type="auto"), C_1: C_3: Buffer(C_2, float32, [m], [stride_2], type="auto")} { + for (i: int32, 0, m) { + C[(i*stride_2)] = ((A[(i*stride)] + 1f32)*2f32) + } +} +``` + +## compute_root + +`compute_root` 可将一个 stage 的计算移动到 root。 + +``` python +A = te.placeholder((m,), name="A") +B = te.compute((m,), lambda i: A[i] + 1, name="B") +C = te.compute((m,), lambda i: B[i] * 2, name="C") + +s = te.create_schedule(C.op) +s[B].compute_at(s[C], C.op.axis[0]) +s[B].compute_root() +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [(stride: int32*m: int32)], [], type="auto"), + B: Buffer(B_2: Pointer(float32), float32, [(stride_1: int32*m)], [], type="auto"), + C: Buffer(C_2: Pointer(float32), float32, [(stride_2: int32*m)], [], type="auto")} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [m], [stride], type="auto"), B_1: B_3: Buffer(B_2, float32, [m], [stride_1], type="auto"), C_1: C_3: Buffer(C_2, float32, [m], [stride_2], type="auto")} { + for (i: int32, 0, m) { + B[(i*stride_1)] = (A[(i*stride)] + 1f32) + } + for (i_1: int32, 0, m) { + C[(i_1*stride_2)] = (B[(i_1*stride_1)]*2f32) + } +} +``` + +## 总结 + +本教程介绍了 TVM 中的 schedule 原语(使得用户可以轻松、灵活地调度计算)。 + +为提高内核的性能,一般的工作流程如下: + +* 通过一系列操作描述你的计算。 +* 使用原语来调度计算。 +* 编译并运行,查看性能差异。 +* 根据运行结果来调整 schedule。 + + +[下载 Python 源代码:schedule_primitives.py](https://tvm.apache.org/docs/_downloads/da47fa2ad30c4b6921171c97e72f36a9/schedule_primitives.py) + +[下载 Jupyter Notebook:schedule_primitives.ipynb](https://tvm.apache.org/docs/_downloads/b78f1a6e1b2c2fb073a791dc258a1d7d/schedule_primitives.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/te_schedules/02-reduction.md b/versioned_docs/version-0.12.0/how_to/te_schedules/02-reduction.md new file mode 100644 index 00000000..af229015 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/te_schedules/02-reduction.md @@ -0,0 +1,366 @@ +--- +title: 规约(reduce) +--- + +# 规约(reduce) + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/work_with_schedules/reduction.html#sphx-glr-download-how-to-work-with-schedules-reduction-py) 下载完整的示例代码 +::: + +**作者**:[Tianqi Chen](https://tqchen.github.io/) + +本文介绍如何在 TVM 中规约(reduce)。关联规约算子(如 sum/max/min)是线性代数运算的典型构造块。 + +``` python +from __future__ import absolute_import, print_function + +import tvm +import tvm.testing +from tvm import te +import numpy as np +``` + +## 描述行的总和 + +在 NumPy 语法中,计算行的总和可以写成 `B = numpy.sum(A, axis=1)` + +下面几行描述了行求和操作。为创建一个规约公式,用 `te.reduce_axis` 声明了一个 reduction 轴,它接收规约的范围。 `te.sum` 接收要规约的表达式以及 reduction 轴,并计算声明范围内所有 k 值的总和。 + +等效的 C 代码如下: + +``` c +for (int i = 0; i < n; ++i) { + B[i] = 0; + for (int k = 0; k < m; ++k) { + B[i] = B[i] + A[i][k]; + } +} +``` + +``` python +n = te.var("n") +m = te.var("m") +A = te.placeholder((n, m), name="A") +k = te.reduce_axis((0, m), "k") +B = te.compute((n,), lambda i: te.sum(A[i, k], axis=k), name="B") +``` + +## Schedule 规约 + +有几种方法可以 Schedule Reduce,先打印出默认 Schedule 的 IR 代码。 + +``` python +s = te.create_schedule(B.op) +print(tvm.lower(s, [A, B], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [(stride: int32*n: int32)], [], type="auto"), + B: Buffer(B_2: Pointer(float32), float32, [(stride_1: int32*n)], [], type="auto")} + buffer_map = {A_1: A, B_1: B} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [n, m: int32], [stride, stride_2: int32], type="auto"), B_1: B_3: Buffer(B_2, float32, [n], [stride_1], type="auto")} { + for (i: int32, 0, n) { + B[(i*stride_1)] = 0f32 + for (k: int32, 0, m) { + B[(i*stride_1)] = (B[(i*stride_1)] + A[((i*stride) + (k*stride_2))]) + } + } +} +``` + +IR 代码与 C 代码非常相似,reduction 轴类似于普通轴,可以拆分。 + +以下代码按不同的因子将 B 的行轴和轴进行拆分,得到一个嵌套 reduction。 + +``` python +ko, ki = s[B].split(B.op.reduce_axis[0], factor=16) +xo, xi = s[B].split(B.op.axis[0], factor=32) +print(tvm.lower(s, [A, B], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [(stride: int32*n: int32)], [], type="auto"), + B: Buffer(B_2: Pointer(float32), float32, [(stride_1: int32*n)], [], type="auto")} + buffer_map = {A_1: A, B_1: B} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [n, m: int32], [stride, stride_2: int32], type="auto"), B_1: B_3: Buffer(B_2, float32, [n], [stride_1], type="auto")} { + for (i.outer: int32, 0, floordiv((n + 31), 32)) { + for (i.inner: int32, 0, 32) { + if @tir.likely((((i.outer*32) + i.inner) < n), dtype=bool) { + B[(((i.outer*32) + i.inner)*stride_1)] = 0f32 + } + if @tir.likely((((i.outer*32) + i.inner) < n), dtype=bool) { + for (k.outer: int32, 0, floordiv((m + 15), 16)) { + for (k.inner: int32, 0, 16) { + if @tir.likely((((k.outer*16) + k.inner) < m), dtype=bool) { + let cse_var_1: int32 = ((i.outer*32) + i.inner) + B[(cse_var_1*stride_1)] = (B[(cse_var_1*stride_1)] + A[((cse_var_1*stride) + (((k.outer*16) + k.inner)*stride_2))]) + } + } + } + } + } + } +} +``` + +把 B 的行绑定到 GPU 线程,从而构建一个 GPU 内核。 + +``` python +s[B].bind(xo, te.thread_axis("blockIdx.x")) +s[B].bind(xi, te.thread_axis("threadIdx.x")) +print(tvm.lower(s, [A, B], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [(stride: int32*n: int32)], [], type="auto"), + B: Buffer(B_2: Pointer(float32), float32, [(stride_1: int32*n)], [], type="auto")} + buffer_map = {A_1: A, B_1: B} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [n, m: int32], [stride, stride_2: int32], type="auto"), B_1: B_3: Buffer(B_2, float32, [n], [stride_1], type="auto")} { + attr [IterVar(blockIdx.x: int32, (nullptr), "ThreadIndex", "blockIdx.x")] "thread_extent" = floordiv((n + 31), 32); + attr [IterVar(threadIdx.x: int32, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 32 { + if @tir.likely((((blockIdx.x*32) + threadIdx.x) < n), dtype=bool) { + B[(((blockIdx.x*32) + threadIdx.x)*stride_1)] = 0f32 + } + for (k.outer: int32, 0, floordiv((m + 15), 16)) { + for (k.inner: int32, 0, 16) { + if @tir.likely((((blockIdx.x*32) + threadIdx.x) < n), dtype=bool) { + if @tir.likely((((k.outer*16) + k.inner) < m), dtype=bool) { + B[(((blockIdx.x*32) + threadIdx.x)*stride_1)] = (B[(((blockIdx.x*32) + threadIdx.x)*stride_1)] + A[((((blockIdx.x*32) + threadIdx.x)*stride) + (((k.outer*16) + k.inner)*stride_2))]) + } + } + } + } + } +} +``` + +## 规约因式分解和并行化 + +构建规约时不能简单地在 reduction 轴上并行化,需要划分规约,将局部规约结果存储在数组中,然后再对临时数组进行规约。 + +rfactor 原语对计算进行了上述重写,在下面的调度中,B 的结果被写入一个临时结果 B.rf,分解后的维度成为 B.rf 的第一个维度。 + +``` python +s = te.create_schedule(B.op) +ko, ki = s[B].split(B.op.reduce_axis[0], factor=16) +BF = s.rfactor(B, ki) +print(tvm.lower(s, [A, B], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [(stride: int32*n: int32)], [], type="auto"), + B: Buffer(B_2: Pointer(float32), float32, [(stride_1: int32*n)], [], type="auto")} + buffer_map = {A_1: A, B_1: B} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [n, m: int32], [stride, stride_2: int32], type="auto"), B_1: B_3: Buffer(B_2, float32, [n], [stride_1], type="auto")} { + allocate(B.rf: Pointer(global float32), float32, [(n*16)]), storage_scope = global { + for (k.inner: int32, 0, 16) { + for (i: int32, 0, n) { + B.rf_1: Buffer(B.rf, float32, [(16*n)], [])[((k.inner*n) + i)] = 0f32 + for (k.outer: int32, 0, floordiv((m + 15), 16)) { + if @tir.likely((((k.outer*16) + k.inner) < m), dtype=bool) { + B.rf_1[((k.inner*n) + i)] = (B.rf_1[((k.inner*n) + i)] + A[((i*stride) + (((k.outer*16) + k.inner)*stride_2))]) + } + } + } + } + for (ax0: int32, 0, n) { + B[(ax0*stride_1)] = 0f32 + for (k.inner.v: int32, 0, 16) { + B[(ax0*stride_1)] = (B[(ax0*stride_1)] + B.rf_1[((k.inner.v*n) + ax0)]) + } + } + } +} +``` + +B 的调度算子被重写为 B.f 的规约结果在第一个轴上的和。 + +``` python +print(s[B].op.body) +``` + +输出结果: + +``` bash +[reduce(combiner=comm_reducer(result=[(x + y)], lhs=[x], rhs=[y], identity_element=[0f]), source=[B.rf[k.inner.v, ax0]], init=[], axis=[iter_var(k.inner.v, range(min=0, ext=16))], where=(bool)1, value_index=0)] +``` + +## 跨线程规约 + +接下来可以在因子轴上进行并行化,这里 B 的 reduction 轴被标记为线程,如果唯一的 reduction 轴在设备中可以进行跨线程规约,则 TVM 允许将 reduction 轴标记为 thread。 + +也可以直接在规约轴上计算 BF。最终生成的内核会将行除以 blockIdx.x,将 threadIdx.y 列除以 threadIdx.x,最后对 threadIdx.x 进行跨线程规约。 + +``` python +xo, xi = s[B].split(s[B].op.axis[0], factor=32) +s[B].bind(xo, te.thread_axis("blockIdx.x")) +s[B].bind(xi, te.thread_axis("threadIdx.y")) +tx = te.thread_axis("threadIdx.x") +s[B].bind(s[B].op.reduce_axis[0], tx) +s[BF].compute_at(s[B], s[B].op.reduce_axis[0]) +s[B].set_store_predicate(tx.var.equal(0)) +fcuda = tvm.build(s, [A, B], "cuda") +print(fcuda.imported_modules[0].get_source()) +``` + +输出结果: + +``` c +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ < 700) +#define __shfl_sync(mask, var, lane, width) \ + __shfl((var), (lane), (width)) + +#define __shfl_down_sync(mask, var, offset, width) \ + __shfl_down((var), (offset), (width)) + +#define __shfl_up_sync(mask, var, offset, width) \ + __shfl_up((var), (offset), (width)) +#endif + +#ifdef _WIN32 + using uint = unsigned int; + using uchar = unsigned char; + using ushort = unsigned short; + using int64_t = long long; + using uint64_t = unsigned long long; +#else + #define uint unsigned int + #define uchar unsigned char + #define ushort unsigned short + #define int64_t long long + #define uint64_t unsigned long long +#endif +extern "C" __global__ void __launch_bounds__(512) default_function_kernel0(float* __restrict__ A, float* __restrict__ B, int m, int n, int stride, int stride1, int stride2) { + float B_rf[1]; + float red_buf0[1]; + B_rf[0] = 0.000000e+00f; + for (int k_outer = 0; k_outer < (m >> 4); ++k_outer) { + if (((((int)blockIdx.x) * 32) + ((int)threadIdx.y)) < n) { + B_rf[0] = (B_rf[0] + A[((((((int)blockIdx.x) * 32) + ((int)threadIdx.y)) * stride) + (((k_outer * 16) + ((int)threadIdx.x)) * stride1))]); + } + } + for (int k_outer1 = 0; k_outer1 < (((m & 15) + 15) >> 4); ++k_outer1) { + if (((((int)blockIdx.x) * 32) + ((int)threadIdx.y)) < n) { + if (((((m >> 4) * 16) + (k_outer1 * 16)) + ((int)threadIdx.x)) < m) { + B_rf[0] = (B_rf[0] + A[((((((int)blockIdx.x) * 32) + ((int)threadIdx.y)) * stride) + (((((m >> 4) * 16) + (k_outer1 * 16)) + ((int)threadIdx.x)) * stride1))]); + } + } + } + uint mask[1]; + float t0[1]; + red_buf0[0] = B_rf[0]; + mask[0] = (__activemask() & ((uint)(65535 << (((int)threadIdx.y) * 16)))); + t0[0] = __shfl_down_sync(mask[0], red_buf0[0], 8, 32); + red_buf0[0] = (red_buf0[0] + t0[0]); + t0[0] = __shfl_down_sync(mask[0], red_buf0[0], 4, 32); + red_buf0[0] = (red_buf0[0] + t0[0]); + t0[0] = __shfl_down_sync(mask[0], red_buf0[0], 2, 32); + red_buf0[0] = (red_buf0[0] + t0[0]); + t0[0] = __shfl_down_sync(mask[0], red_buf0[0], 1, 32); + red_buf0[0] = (red_buf0[0] + t0[0]); + red_buf0[0] = __shfl_sync(mask[0], red_buf0[0], (((int)threadIdx.y) * 16), 32); + if (((int)threadIdx.x) == 0) { + B[(((((int)blockIdx.x) * 32) + ((int)threadIdx.y)) * stride2)] = red_buf0[0]; + } +} +``` + +结果内核与 NumPy 进行比较来验证结果内核的正确性。 + +``` python +nn = 128 +dev = tvm.cuda(0) +a = tvm.nd.array(np.random.uniform(size=(nn, nn)).astype(A.dtype), dev) +b = tvm.nd.array(np.zeros(nn, dtype=B.dtype), dev) +fcuda(a, b) +tvm.testing.assert_allclose(b.numpy(), np.sum(a.numpy(), axis=1), rtol=1e-4) +``` + +## 用二维规约描述卷积 + +在 TVM 中,用简单的二维规约来描述卷积(过滤器大小 = [3, 3],步长 = [1, 1])。 + +``` python +n = te.var("n") +Input = te.placeholder((n, n), name="Input") +Filter = te.placeholder((3, 3), name="Filter") +di = te.reduce_axis((0, 3), name="di") +dj = te.reduce_axis((0, 3), name="dj") +Output = te.compute( + (n - 2, n - 2), + lambda i, j: te.sum(Input[i + di, j + dj] * Filter[di, dj], axis=[di, dj]), + name="Output", +) +s = te.create_schedule(Output.op) +print(tvm.lower(s, [Input, Filter, Output], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(Input_1: handle, Filter_1: handle, Output_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {Input: Buffer(Input_2: Pointer(float32), float32, [(stride: int32*n: int32)], [], type="auto"), + Filter: Buffer(Filter_2: Pointer(float32), float32, [9], []), + Output: Buffer(Output_2: Pointer(float32), float32, [((n - 2)*(n - 2))], [])} + buffer_map = {Input_1: Input, Filter_1: Filter, Output_1: Output} + preflattened_buffer_map = {Input_1: Input_3: Buffer(Input_2, float32, [n, n], [stride, stride_1: int32], type="auto"), Filter_1: Filter_3: Buffer(Filter_2, float32, [3, 3], []), Output_1: Output_3: Buffer(Output_2, float32, [(n - 2), (n - 2)], [])} { + for (i: int32, 0, (n - 2)) { + for (j: int32, 0, (n - 2)) { + Output[((i*(n - 2)) + j)] = 0f32 + for (di: int32, 0, 3) { + for (dj: int32, 0, 3) { + Output[((i*(n - 2)) + j)] = (Output[((i*(n - 2)) + j)] + (Input[(((i + di)*stride) + ((j + dj)*stride_1))]*Filter[((di*3) + dj)])) + } + } + } + } +} +``` + +## 定义一般交换规约运算 + +除了 `te.sum`, `tvm.te.min` 和 `tvm.te.max` 等内置规约操作外,还可以通过 `te.comm_reducer` 定义交换规约操作。 + +``` python +n = te.var("n") +m = te.var("m") +product = te.comm_reducer(lambda x, y: x * y, lambda t: tvm.tir.const(1, dtype=t), name="product") +A = te.placeholder((n, m), name="A") +k = te.reduce_axis((0, m), name="k") +B = te.compute((n,), lambda i: product(A[i, k], axis=k), name="B") +``` + +:::note +执行涉及多个值的规约,例如 `argmax`,可以通过元组输入来完成。更多详细信息,请参阅 [使用协作输入描述规约](https://tvm.apache.org/docs/how_to/work_with_schedules/tuple_inputs.html#reduction-with-tuple-inputs)。 +::: + +## 总结 + +本教程演示了如何规约 schedule。 + +* 用 reduce_axis 描述规约。 +* 如需并行性(parallelism),用 rfactor 来分解轴。 +* 通过 `te.comm_reducer` 定义新的规约操作。 + + +[下载 Python 源代码:reduction.py](https://tvm.apache.org/docs/_downloads/2a0982f8ca0176cb17713d28286536e4/reduction.py) + +[下载 Jupyter Notebook:reduction.ipynb](https://tvm.apache.org/docs/_downloads/10d831d158490a9ee3abd1901806fc11/reduction.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/te_schedules/03-math.md b/versioned_docs/version-0.12.0/how_to/te_schedules/03-math.md new file mode 100644 index 00000000..bffe0def --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/te_schedules/03-math.md @@ -0,0 +1,275 @@ +--- +title: 内联及数学函数 +--- + +# 内联及数学函数 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/work_with_schedules/intrin_math.html#sphx-glr-download-how-to-work-with-schedules-intrin-math-py) 下载完整的示例代码 +::: + +**作者**:[Tianqi Chen](https://tqchen.github.io/) + +尽管 TVM 支持基本的算术运算,但很多时候,也需要复杂的内置函数,例如 `exp` 取指函数。 + +这些函数是依赖 target 系统的,并且在不同 target 平台中可能具有不同的名称。本教程会学习到如何调用这些 target-specific 函数,以及如何通过 TVM 内联 API 统一接口。 + +``` python +from __future__ import absolute_import, print_function + +import numpy as np +import tvm +from tvm import te +from tvm.ir import register_op_attr, register_intrin_lowering +``` + +## 直接声明外部数学调用 + +调用 target-specific 函数最直接方法,就是通过 TVM 中的 extern 函数调用构造。以下示例用 `tvm.tir.call_pure_extern` 来调用 `__expf` 函数(仅在 CUDA 下可用)。 + +``` python +n = te.var("n") +A = te.placeholder((n,), name="A") +B = te.compute(A.shape, lambda i: tvm.tir.call_pure_extern("float32", "__expf", A[i]), name="B") +s = te.create_schedule(B.op) +num_thread = 64 +bx, tx = s[B].split(B.op.axis[0], factor=num_thread) +s[B].bind(bx, te.thread_axis("blockIdx.x")) +s[B].bind(tx, te.thread_axis("threadIdx.x")) +f = tvm.build(s, [A, B], "cuda", name="myexp") +print(f.imported_modules[0].get_source()) +``` + +输出结果: + +``` c +#ifdef _WIN32 + using uint = unsigned int; + using uchar = unsigned char; + using ushort = unsigned short; + using int64_t = long long; + using uint64_t = unsigned long long; +#else + #define uint unsigned int + #define uchar unsigned char + #define ushort unsigned short + #define int64_t long long + #define uint64_t unsigned long long +#endif +extern "C" __global__ void __launch_bounds__(64) myexp_kernel0(float* __restrict__ B, float* __restrict__ A, int n, int stride, int stride1) { + if (((int)blockIdx.x) < (n >> 6)) { + B[(((((int)blockIdx.x) * 64) + ((int)threadIdx.x)) * stride)] = __expf(A[(((((int)blockIdx.x) * 64) + ((int)threadIdx.x)) * stride1)]); + } else { + if (((((int)blockIdx.x) * 64) + ((int)threadIdx.x)) < n) { + B[(((((int)blockIdx.x) * 64) + ((int)threadIdx.x)) * stride)] = __expf(A[(((((int)blockIdx.x) * 64) + ((int)threadIdx.x)) * stride1)]); + } + } +} +``` + +## 统一内联调用 + +以上代码验证了直接外部调用可用于 device-specific 的函数。但上述方式仅适用于带有浮点类型的 CUDA target。理想情况下,我们希望写一套代码,即可适用于任何设备以及任何数据类型。 + +TVM 内联函数为用户提供了实现机制,且推荐用这个方法来解决问题。以下代码用的是 te.exp,它创建了一个内联调用 `tvm.te.exp()` 来做指数。 + +``` python +n = te.var("n") +A = te.placeholder((n,), name="A") +B = te.compute(A.shape, lambda i: te.exp(A[i]), name="B") +s = te.create_schedule(B.op) +num_thread = 64 +bx, tx = s[B].split(B.op.axis[0], factor=num_thread) +s[B].bind(bx, te.thread_axis("blockIdx.x")) +s[B].bind(tx, te.thread_axis("threadIdx.x")) +fcuda = tvm.build(s, [A, B], "cuda", name="myexp") +print(fcuda.imported_modules[0].get_source()) +``` + +输出结果: + +``` c +#ifdef _WIN32 + using uint = unsigned int; + using uchar = unsigned char; + using ushort = unsigned short; + using int64_t = long long; + using uint64_t = unsigned long long; +#else + #define uint unsigned int + #define uchar unsigned char + #define ushort unsigned short + #define int64_t long long + #define uint64_t unsigned long long +#endif +extern "C" __global__ void __launch_bounds__(64) myexp_kernel0(float* __restrict__ B, float* __restrict__ A, int n, int stride, int stride1) { + if (((int)blockIdx.x) < (n >> 6)) { + B[(((((int)blockIdx.x) * 64) + ((int)threadIdx.x)) * stride)] = __expf(A[(((((int)blockIdx.x) * 64) + ((int)threadIdx.x)) * stride1)]); + } else { + if (((((int)blockIdx.x) * 64) + ((int)threadIdx.x)) < n) { + B[(((((int)blockIdx.x) * 64) + ((int)threadIdx.x)) * stride)] = __expf(A[(((((int)blockIdx.x) * 64) + ((int)threadIdx.x)) * stride1)]); + } + } +} +``` + +该代码适用于 CUDA 和 opencl,相同的 te.exp 也可用于 float64 数据类型。 + +``` python +fopencl = tvm.build(s, [A, B], "opencl", name="myexp") +print(fopencl.imported_modules[0].get_source()) +``` + +输出结果: + +``` bash +// Function: myexp_kernel0 +__kernel void myexp_kernel0(__global float* restrict B, __global float* restrict A, int n, int stride, int stride1) { + if (((int)get_group_id(0)) < (n >> 6)) { + B[(((((int)get_group_id(0)) * 64) + ((int)get_local_id(0))) * stride)] = exp(A[(((((int)get_group_id(0)) * 64) + ((int)get_local_id(0))) * stride1)]); + } else { + if (((((int)get_group_id(0)) * 64) + ((int)get_local_id(0))) < n) { + B[(((((int)get_group_id(0)) * 64) + ((int)get_local_id(0))) * stride)] = exp(A[(((((int)get_group_id(0)) * 64) + ((int)get_local_id(0))) * stride1)]); + } + } +} +``` + +## 内联函数降级规则 + +当调用 `tvm.te.exp()` 时,TVM 会创建一个 intrinsic Call Expr。TVM 使用转换规则(transformation rules),将内联调用(intrinsic call)转换为特定设备的外部调用(extern calls)。 + +TVM 支持在运行时自定义规则,以下示例为 `exp` 自定义 CUDA 降级规则。 + +``` python +def my_cuda_math_rule(op): + """自定义 CUDA 内联函数降级规则""" + assert isinstance(op, tvm.tir.Call) + name = op.op.name + assert name.startswith("tir.") + dispatch_name = name[4:] + if op.dtype == "float32": + # 调用浮点函数 + return tvm.tir.call_pure_extern("float32", "%sf" % dispatch_name, op.args[0]) + elif op.dtype == "float64": + # 调用双精度函数 + return tvm.tir.call_pure_extern("float32", dispatch_name, op.args[0]) + else: + # 不能转换,返回自身。 + return op + +register_intrin_lowering("tir.exp", target="cuda", f=my_cuda_math_rule, level=99) +``` + +输出结果: + +``` bash + +``` + +用选项覆盖现有规则,从而将规则注册到 TVM。注意,打印代码与之前代码的区别:新规则用数学函数 `expf`,而不是快速数学版本 `__expf`。 + +``` python +fcuda = tvm.build(s, [A, B], "cuda", name="myexp") +print(fcuda.imported_modules[0].get_source()) +``` + +输出结果: + +``` c +#ifdef _WIN32 + using uint = unsigned int; + using uchar = unsigned char; + using ushort = unsigned short; + using int64_t = long long; + using uint64_t = unsigned long long; +#else + #define uint unsigned int + #define uchar unsigned char + #define ushort unsigned short + #define int64_t long long + #define uint64_t unsigned long long +#endif +extern "C" __global__ void __launch_bounds__(64) myexp_kernel0(float* __restrict__ B, float* __restrict__ A, int n, int stride, int stride1) { + if (((int)blockIdx.x) < (n >> 6)) { + B[(((((int)blockIdx.x) * 64) + ((int)threadIdx.x)) * stride)] = expf(A[(((((int)blockIdx.x) * 64) + ((int)threadIdx.x)) * stride1)]); + } else { + if (((((int)blockIdx.x) * 64) + ((int)threadIdx.x)) < n) { + B[(((((int)blockIdx.x) * 64) + ((int)threadIdx.x)) * stride)] = expf(A[(((((int)blockIdx.x) * 64) + ((int)threadIdx.x)) * stride1)]); + } + } +} +``` + +## 添加内联函数 + +对于 TVM 未提供的内联函数,用户可以借助内联规则系统,添加新的内联函数。以下是将内联函数 `mylog` 添加到系统的示例: + +``` python +def mylog(x): + """自定义日志内联函数""" + return tvm.tir.call_intrin(x.dtype, "tir.mylog", x) + +def my_cuda_mylog_rule(op): + """CUDA 降级日志的规则""" + if op.dtype == "float32": + return tvm.tir.call_pure_extern("float32", "logf", op.args[0]) + elif op.dtype == "float64": + return tvm.tir.call_pure_extern("float64", "log", op.args[0]) + else: + return op + +# 新的注册操作是通过注册操作的属性来触发的 +register_op_attr("tir.mylog", "TCallEffectKind", tvm.tir.CallEffectKind.Pure) +register_intrin_lowering("tir.mylog", target="cuda", f=my_cuda_mylog_rule, level=99) + +n = te.var("n") +A = te.placeholder((n,), name="A") +B = te.compute(A.shape, lambda i: mylog(A[i]), name="B") +s = te.create_schedule(B.op) +num_thread = 64 +bx, tx = s[B].split(B.op.axis[0], factor=num_thread) +s[B].bind(bx, te.thread_axis("blockIdx.x")) +s[B].bind(tx, te.thread_axis("threadIdx.x")) +fcuda = tvm.build(s, [A, B], "cuda", name="mylog") +print(fcuda.imported_modules[0].get_source()) +``` + +输出结果: + +``` c +#ifdef _WIN32 + using uint = unsigned int; + using uchar = unsigned char; + using ushort = unsigned short; + using int64_t = long long; + using uint64_t = unsigned long long; +#else + #define uint unsigned int + #define uchar unsigned char + #define ushort unsigned short + #define int64_t long long + #define uint64_t unsigned long long +#endif +extern "C" __global__ void __launch_bounds__(64) mylog_kernel0(float* __restrict__ B, float* __restrict__ A, int n, int stride, int stride1) { + if (((int)blockIdx.x) < (n >> 6)) { + B[(((((int)blockIdx.x) * 64) + ((int)threadIdx.x)) * stride)] = logf(A[(((((int)blockIdx.x) * 64) + ((int)threadIdx.x)) * stride1)]); + } else { + if (((((int)blockIdx.x) * 64) + ((int)threadIdx.x)) < n) { + B[(((((int)blockIdx.x) * 64) + ((int)threadIdx.x)) * stride)] = logf(A[(((((int)blockIdx.x) * 64) + ((int)threadIdx.x)) * stride1)]); + } + } +} +``` + +## 总结 + +* TVM 能调用依赖 target 的外部数学函数。 +* 用内联函数为函数定义统一的接口。 +* 有关 TVM 中更多可用的内联函数,查看 `tvm.tir`。 +* 通过自定义规则,从而自定义内联行为。 + + +[下载 Python 源代码:intrin_math.py](https://tvm.apache.org/docs/_downloads/d9089082842c138d4c81335f88c60c82/intrin_math.py) + +[下载 Jupyter Notebook:intrin_math.ipynb](https://tvm.apache.org/docs/_downloads/1e482ba1190961191e3a0bdbd0585faa/intrin_math.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/te_schedules/04-scan_recurrent.md b/versioned_docs/version-0.12.0/how_to/te_schedules/04-scan_recurrent.md new file mode 100644 index 00000000..8bccff4f --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/te_schedules/04-scan_recurrent.md @@ -0,0 +1,220 @@ +--- +title: 线性和递归核 +--- + +# 线性和递归核 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/work_with_schedules/scan.html#sphx-glr-download-how-to-work-with-schedules-scan-py) 下载完整的示例代码 +::: + +**作者**:[Tianqi Chen](https://tqchen.github.io/) + +下面介绍如何在 TVM 中进行递归计算(神经网络中的典型模式)。 + +``` python +from __future__ import absolute_import, print_function + +import tvm +import tvm.testing +from tvm import te +import numpy as np +``` + +TVM 用线性算子来描述符号循环。以下线性算子计算 X 列上的累积和。 + +线性在张量的最高维度上进行。`s_state` 是描述线性转换状态的占位符。`s_init` 描述如何初始化前 k 个时间步长,其第一个维度为 1,描述了如何初始化第一个时间步长的状态。 + +`s_update` 描述了如何更新时间步长 t 处的值,更新的值可通过状态占位符引用上一个时间步长的值。注意在当前或之后的时间步长引用 `s_state` 是无效的。 + +线性包含状态占位符、初始值和更新描述。推荐列出线性单元的输入,线性的结果是一个张量—— `s_state` 在时域更新后的结果。 + +``` python +m = te.var("m") +n = te.var("n") +X = te.placeholder((m, n), name="X") +s_state = te.placeholder((m, n)) +s_init = te.compute((1, n), lambda _, i: X[0, i]) +s_update = te.compute((m, n), lambda t, i: s_state[t - 1, i] + X[t, i]) +s_scan = tvm.te.scan(s_init, s_update, s_state, inputs=[X]) +``` + +## 调度线性单元 + +通过分别调度 update 和 init 部分来调度线性体。注意,调度更新部分的第一个迭代维度是无效的。要在时间迭代上拆分,用户可以在 scan_op.scan_axis 上进行调度。 + +``` python +s = te.create_schedule(s_scan.op) +num_thread = 256 +block_x = te.thread_axis("blockIdx.x") +thread_x = te.thread_axis("threadIdx.x") +xo, xi = s[s_init].split(s_init.op.axis[1], factor=num_thread) +s[s_init].bind(xo, block_x) +s[s_init].bind(xi, thread_x) +xo, xi = s[s_update].split(s_update.op.axis[1], factor=num_thread) +s[s_update].bind(xo, block_x) +s[s_update].bind(xi, thread_x) +print(tvm.lower(s, [X, s_scan], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(X_1: handle, scan_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {X: Buffer(X_2: Pointer(float32), float32, [(stride: int32*m: int32)], [], type="auto"), + scan: Buffer(scan_2: Pointer(float32), float32, [(stride_1: int32*m)], [], type="auto")} + buffer_map = {X_1: X, scan_1: scan} + preflattened_buffer_map = {X_1: X_3: Buffer(X_2, float32, [m, n: int32], [stride, stride_2: int32], type="auto"), scan_1: scan_3: Buffer(scan_2, float32, [m, n], [stride_1, stride_3: int32], type="auto")} { + attr [IterVar(blockIdx.x: int32, (nullptr), "ThreadIndex", "blockIdx.x")] "thread_extent" = floordiv((n + 255), 256); + attr [IterVar(threadIdx.x: int32, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 256; + if @tir.likely((((blockIdx.x*256) + threadIdx.x) < n), dtype=bool) { + scan[(((blockIdx.x*256) + threadIdx.x)*stride_3)] = X[(((blockIdx.x*256) + threadIdx.x)*stride_2)] + } + for (scan.idx: int32, 0, (m - 1)) { + attr [IterVar(blockIdx.x, (nullptr), "ThreadIndex", "blockIdx.x")] "thread_extent" = floordiv((n + 255), 256); + attr [IterVar(threadIdx.x, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 256; + if @tir.likely((((blockIdx.x*256) + threadIdx.x) < n), dtype=bool) { + let cse_var_1: int32 = (scan.idx + 1) + scan[((cse_var_1*stride_1) + (((blockIdx.x*256) + threadIdx.x)*stride_3))] = (scan[((scan.idx*stride_1) + (((blockIdx.x*256) + threadIdx.x)*stride_3))] + X[((cse_var_1*stride) + (((blockIdx.x*256) + threadIdx.x)*stride_2))]) + } + } +} +``` + +## 构建和验证 + +可以像其他 TVM 内核一样构建线性内核,这里用 numpy 来验证结果的正确性。 + +``` python +fscan = tvm.build(s, [X, s_scan], "cuda", name="myscan") +dev = tvm.cuda(0) +n = 1024 +m = 10 +a_np = np.random.uniform(size=(m, n)).astype(s_scan.dtype) +a = tvm.nd.array(a_np, dev) +b = tvm.nd.array(np.zeros((m, n), dtype=s_scan.dtype), dev) +fscan(a, b) +tvm.testing.assert_allclose(b.numpy(), np.cumsum(a_np, axis=0)) +``` + +## 多阶段线性单元 + +以上示例用 s_update 中的一个张量计算阶段描述了线性单元,可以在线性单元中使用多个张量级。 + +以下代码演示了有两个阶段操作的线性单元中的线性过程: + +``` python +m = te.var("m") +n = te.var("n") +X = te.placeholder((m, n), name="X") +s_state = te.placeholder((m, n)) +s_init = te.compute((1, n), lambda _, i: X[0, i]) +s_update_s1 = te.compute((m, n), lambda t, i: s_state[t - 1, i] * 2, name="s1") +s_update_s2 = te.compute((m, n), lambda t, i: s_update_s1[t, i] + X[t, i], name="s2") +s_scan = tvm.te.scan(s_init, s_update_s2, s_state, inputs=[X]) +``` + +这些中间张量可以正常调度。为了确保正确性,TVM 创建了一个组约束——禁用线性循环之外的 compute_at 位置的线性体。 + +``` python +s = te.create_schedule(s_scan.op) +xo, xi = s[s_update_s2].split(s_update_s2.op.axis[1], factor=32) +s[s_update_s1].compute_at(s[s_update_s2], xo) +``` + +输出结果: + +``` bash +print(tvm.lower(s, [X, s_scan], simple_mode=True)) +@main = primfn(X_1: handle, scan_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {X: Buffer(X_2: Pointer(float32), float32, [(stride: int32*m: int32)], [], type="auto"), + scan: Buffer(scan_2: Pointer(float32), float32, [(stride_1: int32*m)], [], type="auto")} + buffer_map = {X_1: X, scan_1: scan} + preflattened_buffer_map = {X_1: X_3: Buffer(X_2, float32, [m, n: int32], [stride, stride_2: int32], type="auto"), scan_1: scan_3: Buffer(scan_2, float32, [m, n], [stride_1, stride_3: int32], type="auto")} { + allocate(s1: Pointer(global float32), float32, [32]), storage_scope = global { + for (i: int32, 0, n) { + scan[(i*stride_3)] = X[(i*stride_2)] + } + for (scan.idx: int32, 0, (m - 1)) { + for (i.outer: int32, 0, floordiv((n + 31), 32)) { + for (i_1: int32, 0, 32) { + if @tir.likely((((i.outer*32) + i_1) < n), dtype=bool) { + s1_1: Buffer(s1, float32, [32], [])[i_1] = (scan[((scan.idx*stride_1) + (((i.outer*32) + i_1)*stride_3))]*2f32) + } + } + for (i.inner: int32, 0, 32) { + if @tir.likely((((i.outer*32) + i.inner) < n), dtype=bool) { + let cse_var_2: int32 = (scan.idx + 1) + let cse_var_1: int32 = ((i.outer*32) + i.inner) + scan[((cse_var_2*stride_1) + (cse_var_1*stride_3))] = (s1_1[i.inner] + X[((cse_var_2*stride) + (cse_var_1*stride_2))]) + } + } + } + } + } +} +``` + +## 多状态 + +对于像 RNN 这样的复杂应用,需要多个递归状态。线性支持多个递归状态,以下示例演示如何构建具有两种状态的递归。 + +``` python +m = te.var("m") +n = te.var("n") +l = te.var("l") +X = te.placeholder((m, n), name="X") +s_state1 = te.placeholder((m, n)) +s_state2 = te.placeholder((m, l)) +s_init1 = te.compute((1, n), lambda _, i: X[0, i]) +s_init2 = te.compute((1, l), lambda _, i: 0.0) +s_update1 = te.compute((m, n), lambda t, i: s_state1[t - 1, i] + X[t, i]) +s_update2 = te.compute((m, l), lambda t, i: s_state2[t - 1, i] + s_state1[t - 1, 0]) +s_scan1, s_scan2 = tvm.te.scan( + [s_init1, s_init2], [s_update1, s_update2], [s_state1, s_state2], inputs=[X] +) +s = te.create_schedule(s_scan1.op) +print(tvm.lower(s, [X, s_scan1, s_scan2], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(X_1: handle, scan_2: handle, scan_3: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {X: Buffer(X_2: Pointer(float32), float32, [(stride: int32*m: int32)], [], type="auto"), + scan: Buffer(scan_4: Pointer(float32), float32, [(stride_1: int32*m)], [], type="auto"), + scan_1: Buffer(scan_5: Pointer(float32), float32, [(stride_2: int32*m)], [], type="auto")} + buffer_map = {X_1: X, scan_2: scan, scan_3: scan_1} + preflattened_buffer_map = {X_1: X_3: Buffer(X_2, float32, [m, n: int32], [stride, stride_3: int32], type="auto"), scan_2: scan_6: Buffer(scan_4, float32, [m, n], [stride_1, stride_4: int32], type="auto"), scan_3: scan_7: Buffer(scan_5, float32, [m, l: int32], [stride_2, stride_5: int32], type="auto")} { + for (i: int32, 0, n) { + scan[(i*stride_4)] = X[(i*stride_3)] + } + for (i_1: int32, 0, l) { + scan_1[(i_1*stride_5)] = 0f32 + } + for (scan.idx: int32, 0, (m - 1)) { + for (i_2: int32, 0, n) { + let cse_var_1: int32 = (scan.idx + 1) + scan[((cse_var_1*stride_1) + (i_2*stride_4))] = (scan[((scan.idx*stride_1) + (i_2*stride_4))] + X[((cse_var_1*stride) + (i_2*stride_3))]) + } + for (i_3: int32, 0, l) { + scan_1[(((scan.idx + 1)*stride_2) + (i_3*stride_5))] = (scan_1[((scan.idx*stride_2) + (i_3*stride_5))] + scan[(scan.idx*stride_1)]) + } + } +} +``` + +## 总结 + +本教程演示了如何使用线性原语。 + +* 用 init 和 update 描述线性。 +* 将线性单元当作正常 schedule 进行调度。 +* 对于复杂的工作负载,在线性单元中使用多个状态和步骤。 + +[下载 Python 源代码:scan.py](https://tvm.apache.org/docs/_downloads/8c7d8fd6a4b93bcff1f5573943dd02f4/scan.py) + +[下载 Jupyter Notebook:scan.ipynb](https://tvm.apache.org/docs/_downloads/729378592a96230b4f7be71b44da43a4/scan.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/te_schedules/05-external_tensor.md b/versioned_docs/version-0.12.0/how_to/te_schedules/05-external_tensor.md new file mode 100644 index 00000000..1b1fee42 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/te_schedules/05-external_tensor.md @@ -0,0 +1,125 @@ +--- +title: 外部张量函数 +--- + +# 外部张量函数 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/work_with_schedules/extern_op.html#sphx-glr-download-how-to-work-with-schedules-extern-op-py) 下载完整的示例代码 +::: + +**作者**:[Tianqi Chen](https://tqchen.github.io/) + +虽然 TVM 支持透明代码生成,但有时也需将手写的代码合并到流水线,例如对一些卷积核使用 cuDNN,并定义其余阶段。 + +原生 TVM 就支持黑盒函数调用。具体来说,TVM 支持所有与 DLPack 兼容的张量函数。这意味着可以使用 POD 类型(指针、整数、浮点数),或者将指向 DLTensor 的指针作为参数,调用任何函数。 + +``` python +from __future__ import absolute_import, print_function + +import tvm +from tvm import te +import numpy as np +from tvm.contrib import cblas +import tvm.testing + +if not tvm.get_global_func("tvm.contrib.cblas.matmul", allow_missing=True): + raise Exception("Not compiled with cblas support; can't build this tutorial") +``` + +## 使用外部张量函数 + +以下示例用 `te.extern` 来添加一个外部数组函数调用。外部调用声明了输出张量的 shape,第二个参数给出了输入列表。 + +用户需要提供一个描述如何对结果进行计算的函数。计算函数获取输入和输出的符号占位符列表,并返回执行语句。 + +这种情况只需调用一个注册的 TVM 函数,它会调用 CBLAS。TVM 不控制外部数组函数的内部,将其视为黑盒。可以进一步混合可调度的 TVM 函数,为结果添加偏差项。 + +``` python +n = 1024 +l = 128 +m = 235 +bias = te.var("bias", dtype="float32") +A = te.placeholder((n, l), name="A") +B = te.placeholder((l, m), name="B") +C = te.extern( + (n, m), + [A, B], + lambda ins, outs: tvm.tir.call_packed( + "tvm.contrib.cblas.matmul", ins[0], ins[1], outs[0], False, False + ), + name="C", +) +D = te.compute(C.shape, lambda i, j: C[i, j] + bias, name="D") +s = te.create_schedule(D.op) +``` + +## 验证结果 + +验证结果是否符合预期。 + +``` python +dev = tvm.cpu(0) +f = tvm.build(s, [A, B, D, bias], "llvm") +a = tvm.nd.array(np.random.uniform(size=(n, l)).astype(A.dtype), dev) +b = tvm.nd.array(np.random.uniform(size=(l, m)).astype(B.dtype), dev) +d = tvm.nd.array(np.zeros((n, m), dtype=D.dtype), dev) +bb = 10.0 +f(a, b, d, bb) +tvm.testing.assert_allclose(d.numpy(), np.dot(a.numpy(), b.numpy()) + 10, rtol=1e-5) +``` + +## 外部 Contrib Wrappers + +TVM 为外部调用提供了外部contrib Wrappers,以下代码与前面的示例等效。 + +``` python +from tvm.contrib import cblas + +C = cblas.matmul(A, B) +D = te.compute(C.shape, lambda i, j: C[i, j] + bias, name="D") +s = te.create_schedule(D.op) +``` + +## 将 Python 函数 Hook 为 Extern + +由于可以调用 TVM 中的任何 PackedFunc,所以可以用外部函数回调到 Python 中。 + +以下示例将一个 Python 函数注册到 TVM runtime 系统,并用它来完成一个阶段的计算,这使得 TVM 更加灵活。例如,可通过插入前端回调来检查中间结果,或将自定义代码与 TVM 混合。 + +``` python +@tvm.register_func("tvm.contrib.my_tvm_addone") +def my_tvm_addone(x, y): + print("my_tvm_addone signatures: %s, %s" % (type(x), type(y))) + tvm.nd.array(x.numpy() + 1).copyto(y) + +A = te.placeholder((n,), name="A") +B = te.extern( + A.shape, + [A], + lambda ins, outs: tvm.tir.call_packed("tvm.contrib.my_tvm_addone", ins[0], outs[0]), + name="C", +) +s = te.create_schedule(B.op) +f = tvm.build(s, [A, B], "llvm") +a = tvm.nd.array(np.random.uniform(size=(n,)).astype(A.dtype), dev) +b = tvm.nd.array(np.random.uniform(size=(n,)).astype(B.dtype), dev) +f(a, b) +tvm.testing.assert_allclose(b.numpy(), a.numpy() + 1, rtol=1e-5) +``` + +输出结果: + +``` bash +my_tvm_addone signatures: , +``` + +## 总结 + +* TVM 通过 `te.extern` 调用外部张量函数。 +* 对外部张量调用使用 contrib wrappers。 +* 将前端函数 hook 为外部张量的回调。 + +[下载 Python 源代码:extern_op.py](https://tvm.apache.org/docs/_downloads/286e7f77f494a25312ac88e3f234822e/extern_op.py) + +[下载 Jupyter Notebook:extern_op.ipynb](https://tvm.apache.org/docs/_downloads/8472bea81cf679760d7e4e77e895726f/extern_op.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/te_schedules/06-tensorize.md b/versioned_docs/version-0.12.0/how_to/te_schedules/06-tensorize.md new file mode 100644 index 00000000..23d86b04 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/te_schedules/06-tensorize.md @@ -0,0 +1,341 @@ +--- +title: 使用 Tensorize 来利用硬件内联函数 +--- + +# 使用 Tensorize 来利用硬件内联函数 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/work_with_schedules/tensorize.html#sphx-glr-download-how-to-work-with-schedules-tensorize-py) 下载完整的示例代码 +::: + +**作者**:[Yizhi Liu](https://github.com/yzhliu) + +本文介绍如何在 TVM 中进行张量化。 + +通过使用调度原语 `tensorize`,可以用相应的内联函数替换一个计算单元,从而可以利用手写的 micro-kernels,以及扩展 TVM 支持新的硬件架构。 + +本教程的目的是展示 tensorize 的功能和用法,而非提供有效的解决方案。 + +``` python +from __future__ import absolute_import, print_function + +import tvm +from tvm import te +import tvm.testing +import numpy as np +``` + +## 定义矩阵乘法 + +以矩阵乘法为例,Matmul 首先将两个矩阵之间的对应元素相乘,然后在某个轴上累加。以下代码描述了 TVM 中的计算 `A * B^T`。 + +``` python +N, M, L = 1024, 512, 64 +A = te.placeholder((N, L), name="A") +B = te.placeholder((M, L), name="B") +k = te.reduce_axis((0, L), name="k") +C = te.compute((N, M), lambda i, j: te.sum(A[i, k] * B[j, k], axis=k), name="C") +s = te.create_schedule(C.op) +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [65536], []), + B: Buffer(B_2: Pointer(float32), float32, [32768], []), + C: Buffer(C_2: Pointer(float32), float32, [524288], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [1024, 64], []), B_1: B_3: Buffer(B_2, float32, [512, 64], []), C_1: C_3: Buffer(C_2, float32, [1024, 512], [])} { + for (i: int32, 0, 1024) { + for (j: int32, 0, 512) { + C[((i*512) + j)] = 0f32 + for (k: int32, 0, 64) { + let cse_var_1: int32 = ((i*512) + j) + C[cse_var_1] = (C[cse_var_1] + (A[((i*64) + k)]*B[((j*64) + k)])) + } + } + } +} +``` + +## 调度 Matmul + +假设有一个支持矩阵向量乘法(GEMV)作为硬件原语的加速器,它可以采用任意大小的 reduce 轴,但另一个轴需要不大于 16。我们需要分解 matmul 循环,使最里面的循环是(16x64)GEMV。 + +``` python +factor = 16 +x, y = C.op.axis +(z,) = C.op.reduce_axis +yo, yi = s[C].split(y, factor=factor) +s[C].reorder(x, yo, yi, z) +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +```plain +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [65536], []), + B: Buffer(B_2: Pointer(float32), float32, [32768], []), + C: Buffer(C_2: Pointer(float32), float32, [524288], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [1024, 64], []), B_1: B_3: Buffer(B_2, float32, [512, 64], []), C_1: C_3: Buffer(C_2, float32, [1024, 512], [])} { + for (i: int32, 0, 1024) { + for (j.outer: int32, 0, 32) { + for (j.inner: int32, 0, 16) { + C[(((i*512) + (j.outer*16)) + j.inner)] = 0f32 + for (k: int32, 0, 64) { + let cse_var_1: int32 = (((i*512) + (j.outer*16)) + j.inner) + C[cse_var_1] = (C[cse_var_1] + (A[((i*64) + k)]*B[(((j.outer*1024) + (j.inner*64)) + k)])) + } + } + } + } +} +``` + +如上面打印的 IR 所示,内部循环 `j.inner` 与 `k` 共同构成 GEMV 的计算——在最里面的两个循环中,索引 `i` 是固定的,对矩阵 `A` 的访问只取决于 `k`,这使得 `A` 的访问模式是一个「向量」。可以张量化 `j.inner`,从而利用假定硬件的 GEMV 指令。 + +## 定义 GEMV Tensorization 内联函数 + +调度张量之前,首先定义 GEMV 的内联函数。它包括两部分:第一部分是 GEMV 的计算定义, TVM 使用它来匹配原始 Matmul schedule 中的计算模式;二是指定如何在设备上执行 GEMV,在下面的 `intrin_func` 中完成。 + +``` python +def intrin_gemv(m, l): + a = te.placeholder((l,), name="a") + b = te.placeholder((m, l), name="b") + k = te.reduce_axis((0, l), name="k") + c = te.compute((m,), lambda i: te.sum(a[k] * b[i, k], axis=k), name="c") + Ab = tvm.tir.decl_buffer(a.shape, a.dtype, name="A", offset_factor=1, strides=[1]) + Bb = tvm.tir.decl_buffer(b.shape, b.dtype, name="B", offset_factor=1, strides=[te.var("s1"), 1]) + Cb = tvm.tir.decl_buffer(c.shape, c.dtype, name="C", offset_factor=1, strides=[1]) + + def intrin_func(ins, outs): + ib = tvm.tir.ir_builder.create() + aa, bb = ins + cc = outs[0] + ib.emit( + tvm.tir.call_extern( + "int32", + "gemv_update", + cc.access_ptr("w"), + aa.access_ptr("r"), + bb.access_ptr("r"), + m, + l, + bb.strides[0], + ) + ) + return ib.get() + + return te.decl_tensor_intrin(c.op, intrin_func, binds={a: Ab, b: Bb, c: Cb}) +``` + +这里 `te.decl_tensor_intrin` 声明了如何执行计算 `c.op`。我们的实现只是接收输入和输出,将它们转换为指针,并提供外部函数调用。 + +注意,tensorization 需要用户指定 `offset_factor`,TVM 通过这个信息知道数据在原始数据结构的起始地址与传给 tensorize 的偏移量之间是否对齐,因此它有机会通过向量化加载进行优化。简单起见,将因子设置为 1。 + +为输入和输出声明 buffer,这并不是必需的,但如此一来,我们就能受益于 buffer 提供的额外信息了。例如,将 `bb.strides[0]` 作为参数,传给外部函数 `gemv_update`。现在 `bb.strides[0] == l`,稍后将看到它们与更复杂的 schedules 有何不同。 + +注意,将 `te.var("s1")` 作为 `B` 的第一个步长维度。若可以推断步长——在这种情况下,TVM 知道张量 B 是紧凑的,因此步长是 `[L, 1]`——这样的占位符可以让 TVM 自动绑定推断值。 + +``` python +gemv = intrin_gemv(factor, L) +s[C].tensorize(yi, gemv) +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [65536], []), + B: Buffer(B_2: Pointer(float32), float32, [32768], []), + C: Buffer(C_2: Pointer(float32), float32, [524288], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [1024, 64], []), B_1: B_3: Buffer(B_2, float32, [512, 64], []), C_1: C_3: Buffer(C_2, float32, [1024, 512], [])} { + for (i: int32, 0, 1024) { + for (j.outer: int32, 0, 32) { + @tir.call_extern("gemv_update", @tir.tvm_access_ptr(@tir.type_annotation(, dtype=float32), C_2, ((i*512) + (j.outer*16)), 16, 2, dtype=handle), @tir.tvm_access_ptr(@tir.type_annotation(, dtype=float32), A_2, (i*64), 64, 1, dtype=handle), @tir.tvm_access_ptr(@tir.type_annotation(, dtype=float32), B_2, (j.outer*1024), 1024, 1, dtype=handle), 16, 64, 64, dtype=int32) + } + } +} +``` + +通过张量化 `yi`,最里面的两个循环现在被之前定义的内联函数所取代。为了构建和运行模块,定义外部函数 `gemv_update`(GEMV 的简单实现,仅用于演示)。 + +``` python +def gemv_impl(): + cc_code = """ + extern "C" int gemv_update(float *cc, float *aa, float *bb, int m, int l, int stride) { + for (int i = 0; i < m; ++i) { + for (int j = 0; j < l; ++j) { + cc[i] += aa[j] * bb[i * stride + j]; + } + } + return 0; + } + """ + from tvm.contrib import utils, clang + + temp = utils.tempdir() + ll_path = temp.relpath("temp.ll") + # 从 C 源代码创建 LLVM ir + ll_code = clang.create_llvm(cc_code, output=ll_path) + return ll_code +``` + +执行张量化 GEMV 前,利用编译指示属性 `import_llvm` 导入 llvm 内联 asm。 + +``` python +s[C].pragma(x, "import_llvm", gemv_impl()) +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [65536], []), + B: Buffer(B_2: Pointer(float32), float32, [32768], []), + C: Buffer(C_2: Pointer(float32), float32, [524288], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [1024, 64], []), B_1: B_3: Buffer(B_2, float32, [512, 64], []), C_1: C_3: Buffer(C_2, float32, [1024, 512], [])} { + attr [IterVar(i: int32, (nullptr), "DataPar", "")] "pragma_import_llvm" = "; ModuleID = '/tmp/tmpnmkhyqx0/input0.cc'\nsource_filename = \"/tmp/tmpnmkhyqx0/input0.cc\"\ntarget datalayout = \"e-m:e-i64:64-f80:128-n8:16:32:64-S128\"\ntarget triple = \"x86_64-pc-linux-gnu\"\n\n; Function Attrs: noinline nounwind optnone uwtable\ndefine dso_local i32 @gemv_update(float*, float*, float*, i32, i32, i32) #0 {\n %7 = alloca float*, align 8\n %8 = alloca float*, align 8\n %9 = alloca float*, align 8\n %10 = alloca i32, align 4\n %11 = alloca i32, align 4\n %12 = alloca i32, align 4\n %13 = alloca i32, align 4\n %14 = alloca i32, align 4\n store float* %0, float** %7, align 8\n store float* %1, float** %8, align 8\n store float* %2, float** %9, align 8\n store i32 %3, i32* %10, align 4\n store i32 %4, i32* %11, align 4\n store i32 %5, i32* %12, align 4\n store i32 0, i32* %13, align 4\n br label %15\n\n15: ; preds = %50, %6\n %16 = load i32, i32* %13, align 4\n %17 = load i32, i32* %10, align 4\n %18 = icmp slt i32 %16, %17\n br i1 %18, label %19, label %53\n\n19: ; preds = %15\n store i32 0, i32* %14, align 4\n br label %20\n\n20: ; preds = %46, %19\n %21 = load i32, i32* %14, align 4\n %22 = load i32, i32* %11, align 4\n %23 = icmp slt i32 %21, %22\n br i1 %23, label %24, label %49\n\n24: ; preds = %20\n %25 = load float*, float** %8, align 8\n %26 = load i32, i32* %14, align 4\n %27 = sext i32 %26 to i64\n %28 = getelementptr inbounds float, float* %25, i64 %27\n %29 = load float, float* %28, align 4\n %30 = load float*, float** %9, align 8\n %31 = load i32, i32* %13, align 4\n %32 = load i32, i32* %12, align 4\n %33 = mul nsw i32 %31, %32\n %34 = load i32, i32* %14, align 4\n %35 = add nsw i32 %33, %34\n %36 = sext i32 %35 to i64\n %37 = getelementptr inbounds float, float* %30, i64 %36\n %38 = load float, float* %37, align 4\n %39 = fmul float %29, %38\n %40 = load float*, float** %7, align 8\n %41 = load i32, i32* %13, align 4\n %42 = sext i32 %41 to i64\n %43 = getelementptr inbounds float, float* %40, i64 %42\n %44 = load float, float* %43, align 4\n %45 = fadd float %44, %39\n store float %45, float* %43, align 4\n br label %46\n\n46: ; preds = %24\n %47 = load i32, i32* %14, align 4\n %48 = add nsw i32 %47, 1\n store i32 %48, i32* %14, align 4\n br label %20\n\n49: ; preds = %20\n br label %50\n\n50: ; preds = %49\n %51 = load i32, i32* %13, align 4\n %52 = add nsw i32 %51, 1\n store i32 %52, i32* %13, align 4\n br label %15\n\n53: ; preds = %15\n ret i32 0\n}\n\nattributes #0 = { noinline nounwind optnone uwtable \"correctly-rounded-divide-sqrt-fp-math\"=\"false\" \"disable-tail-calls\"=\"false\" \"less-precise-fpmad\"=\"false\" \"min-legal-vector-width\"=\"0\" \"no-frame-pointer-elim\"=\"true\" \"no-frame-pointer-elim-non-leaf\" \"no-infs-fp-math\"=\"false\" \"no-jump-tables\"=\"false\" \"no-nans-fp-math\"=\"false\" \"no-signed-zeros-fp-math\"=\"false\" \"no-trapping-math\"=\"false\" \"stack-protector-buffer-size\"=\"8\" \"target-cpu\"=\"x86-64\" \"target-features\"=\"+cx8,+fxsr,+mmx,+sse,+sse2,+x87\" \"unsafe-fp-math\"=\"false\" \"use-soft-float\"=\"false\" }\n\n!llvm.module.flags = !{!0}\n!llvm.ident = !{!1}\n\n!0 = !{i32 1, !\"wchar_size\", i32 4}\n!1 = !{!\"clang version 9.0.0-2~ubuntu18.04.2 (tags/RELEASE_900/final)\"}\n"; + for (i, 0, 1024) { + for (j.outer: int32, 0, 32) { + @tir.call_extern("gemv_update", @tir.tvm_access_ptr(@tir.type_annotation(, dtype=float32), C_2, ((i*512) + (j.outer*16)), 16, 2, dtype=handle), @tir.tvm_access_ptr(@tir.type_annotation(, dtype=float32), A_2, (i*64), 64, 1, dtype=handle), @tir.tvm_access_ptr(@tir.type_annotation(, dtype=float32), B_2, (j.outer*1024), 1024, 1, dtype=handle), 16, 64, 64, dtype=int32) + } + } +} +``` + +最后,将张量化的版本与 `numpy.dot` 生成的版本进行比较,确保实现是正确的。 + +``` python +func = tvm.build(s, [A, B, C], target="llvm", name="gemv") + +from tvm.topi.utils import get_const_tuple + +dtype = A.dtype +dev = tvm.device("cpu", 0) +a = np.random.uniform(size=get_const_tuple(A.shape)).astype(dtype) +b = np.random.uniform(size=get_const_tuple(B.shape)).astype(dtype) +c = tvm.nd.array(np.zeros(get_const_tuple(C.shape), dtype=dtype), dev) +func(tvm.nd.array(a, dev), tvm.nd.array(b, dev), c) +tvm.testing.assert_allclose(c.numpy(), np.dot(a, b.T), rtol=1e-3) +``` + +## 更新 tensorize 的 reduce + +前面已经了解了 tensorize 的基本概念,接下来看一个更复杂的案例。 + +假设加速器只能让一个向量和一个矩阵相乘,其中向量大小不大于 16。鉴于这样的硬件约束,需要按如下方式将 reduce 轴拆分: + +``` python +zo, zi = s[C].split(z, factor=factor) +s[C].reorder(x, yo, zo, yi, zi) +``` + +由于 tensorize 内联函数目前只覆盖了 reduce 轴的一部分,而非使用「body」函数,因此 TVM 需要一个 `reduce_reset` 函数(在 reduce for 循环之前调用),以及一个 `reduce_update` 函数(定义了「update」计算策略)。 + +``` python +def gemv_impl(): + cc_code = """ + extern "C" int gemv_update(float *cc, float *aa, float *bb, int m, int l, int stride) { + for (int i = 0; i < m; ++i) { + for (int j = 0; j < l; ++j) { + cc[i] += aa[j] * bb[i * stride + j]; + } + } + return 0; + } + extern "C" int gemv_reset(float *cc, int m) { + for (int i = 0; i < m; ++i) { + cc[i] = 0.0; + } + return 0; + } + """ + from tvm.contrib import utils, clang + + temp = utils.tempdir() + ll_path = temp.relpath("temp.ll") + # 从 C 源代码创建 LLVM ir + ll_code = clang.create_llvm(cc_code, output=ll_path) + return ll_code + +def intrin_gemv(m, l): + a = te.placeholder((l,), name="a") + b = te.placeholder((m, l), name="b") + k = te.reduce_axis((0, l), name="k") + c = te.compute((m,), lambda i: te.sum(a[k] * b[i, k], axis=k), name="c") + Ab = tvm.tir.decl_buffer(a.shape, a.dtype, name="A", offset_factor=1, strides=[1]) + Bb = tvm.tir.decl_buffer(b.shape, b.dtype, name="B", offset_factor=1, strides=[te.var("s1"), 1]) + Cb = tvm.tir.decl_buffer(c.shape, c.dtype, name="C", offset_factor=1, strides=[1]) + + def intrin_func(ins, outs): + aa, bb = ins + cc = outs[0] + + def _body(): + ib = tvm.tir.ir_builder.create() + ib.emit( + tvm.tir.call_extern( + "int32", + "gemv_update", + cc.access_ptr("w"), + aa.access_ptr("r"), + bb.access_ptr("r"), + m, + l, + bb.strides[0], + ) + ) + return ib.get() + + def _reduce_reset(): + ib = tvm.tir.ir_builder.create() + ib.emit(tvm.tir.call_extern("int32", "gemv_reset", cc.access_ptr("w"), m)) + return ib.get() + + def _reduce_update(): + return _body() + + return _body(), _reduce_reset(), _reduce_update() + + return te.decl_tensor_intrin(c.op, intrin_func, binds={a: Ab, b: Bb, c: Cb}) +``` + +注意,`intrin_func` 返回一个三元组:`(body, reduce_reset, reduce_update)`。如果 tensorization 包括所有 reduce 轴,将调用函数 `body()`,否则 `reduce_reset()` 和 `reduce_update()` 将一起使用。 + +示例中 `body()` 和 `reduce_update()` 的实现相同,在其他情况下,硬件对这两个函数的指令可能不同。此外,可以看到 `bb.strides[0]` 由于平铺而与 `l` 不同。 + +对 squared GEMV 进行 tensorize,构建并检查结果。 + +``` python +gemv = intrin_gemv(factor, factor) +s[C].tensorize(yi, gemv) +s[C].pragma(yo, "import_llvm", gemv_impl()) + +func = tvm.build(s, [A, B, C], target="llvm", name="gemv") +a = np.random.uniform(size=get_const_tuple(A.shape)).astype(dtype) +b = np.random.uniform(size=get_const_tuple(B.shape)).astype(dtype) +c = tvm.nd.array(np.zeros(get_const_tuple(C.shape), dtype=dtype), dev) +func(tvm.nd.array(a, dev), tvm.nd.array(b, dev), c) +tvm.testing.assert_allclose(c.numpy(), np.dot(a, b.T), rtol=1e-3) +``` + +## 总结 + +本教程演示了 TVM 中 tensorize 内联函数的用法。tensorize 提供了一种方法,使得用户通过 micro-kernels 获得完全优化调度。例如,Intel CPU 上的 INT8 量化使用 tensorize 直接调用 AVX 指令。此外,它还使 TVM 能够编译为 ASIC - 查看 [VTA: Versatile Tensor Accelerator](https://tvm.apache.org/docs/topic/vta/index.html#vta-index) 获取详细信息。文档还演示了如何使用内联汇编导入,使用户轻松地将 asm 注入到调度中。 + +[下载 Python 源代码:tensorize.py](https://tvm.apache.org/docs/_downloads/428c6201e29ce74e73c6b41eee589f62/tensorize.py) + +[下载 Jupyter Notebook:tensorize.ipynb](https://tvm.apache.org/docs/_downloads/3b5e41b16a898b72d18127ebe2182c66/tensorize.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/te_schedules/07-compute_reduce.md b/versioned_docs/version-0.12.0/how_to/te_schedules/07-compute_reduce.md new file mode 100644 index 00000000..0ff8ffc2 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/te_schedules/07-compute_reduce.md @@ -0,0 +1,169 @@ +--- +title: 使用元组输入(Tuple Inputs)进行计算和归约 +--- + +# 使用元组输入(Tuple Inputs)进行计算和归约 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/work_with_schedules/tuple_inputs.html#sphx-glr-download-how-to-work-with-schedules-tuple-inputs-py) 下载完整的示例代码 +::: + +**作者**:[Ziheng Jiang](https://github.com/ZihengJiang) + +若要在单个循环中计算具有相同 shape 的多个输出,或执行多个值的归约,例如 `argmax`。这些问题可以通过元组输入来解决。 + +本教程介绍了 TVM 中元组输入的用法。 + +```plain +from __future__ import absolute_import, print_function + +import tvm +from tvm import te +import numpy as np +``` + +## 描述批量计算 + +对于 shape 相同的算子,若要在下一个调度过程中一起调度,可以将它们放在一起作为 `te.compute` 的输入。 + +``` python +n = te.var("n") +m = te.var("m") +A0 = te.placeholder((m, n), name="A0") +A1 = te.placeholder((m, n), name="A1") +B0, B1 = te.compute((m, n), lambda i, j: (A0[i, j] + 2, A1[i, j] * 3), name="B") + +# 生成的 IR 代码: +s = te.create_schedule(B0.op) +print(tvm.lower(s, [A0, A1, B0, B1], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A0_1: handle, A1_1: handle, B_2: handle, B_3: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A0: Buffer(A0_2: Pointer(float32), float32, [(stride: int32*m: int32)], [], type="auto"), + A1: Buffer(A1_2: Pointer(float32), float32, [(stride_1: int32*m)], [], type="auto"), + B: Buffer(B_4: Pointer(float32), float32, [(stride_2: int32*m)], [], type="auto"), + B_1: Buffer(B_5: Pointer(float32), float32, [(stride_3: int32*m)], [], type="auto")} + buffer_map = {A0_1: A0, A1_1: A1, B_2: B, B_3: B_1} + preflattened_buffer_map = {A0_1: A0_3: Buffer(A0_2, float32, [m, n: int32], [stride, stride_4: int32], type="auto"), A1_1: A1_3: Buffer(A1_2, float32, [m, n], [stride_1, stride_5: int32], type="auto"), B_2: B_6: Buffer(B_4, float32, [m, n], [stride_2, stride_6: int32], type="auto"), B_3: B_7: Buffer(B_5, float32, [m, n], [stride_3, stride_7: int32], type="auto")} { + for (i: int32, 0, m) { + for (j: int32, 0, n) { + B[((i*stride_2) + (j*stride_6))] = (A0[((i*stride) + (j*stride_4))] + 2f32) + B_1[((i*stride_3) + (j*stride_7))] = (A1[((i*stride_1) + (j*stride_5))]*3f32) + } + } +} +``` + +## 使用协同输入(Collaborative Inputs)描述归约 + +有时需要多个输入来表达归约算子,并且输入会协同工作,例如 `argmax`。在归约过程中,`argmax` 要比较操作数的值,还需要保留操作数的索引,可用 `te.comm_reducer()` 表示: + +``` python +# x 和 y 是归约的操作数,它们都是元组的索引和值。 +def fcombine(x, y): + lhs = tvm.tir.Select((x[1] >= y[1]), x[0], y[0]) + rhs = tvm.tir.Select((x[1] >= y[1]), x[1], y[1]) + return lhs, rhs + +# 身份元素也要是一个元组,所以 `fidentity` 接收两种类型作为输入。 +def fidentity(t0, t1): + return tvm.tir.const(-1, t0), tvm.te.min_value(t1) + +argmax = te.comm_reducer(fcombine, fidentity, name="argmax") + +# 描述归约计算 +m = te.var("m") +n = te.var("n") +idx = te.placeholder((m, n), name="idx", dtype="int32") +val = te.placeholder((m, n), name="val", dtype="int32") +k = te.reduce_axis((0, n), "k") +T0, T1 = te.compute((m,), lambda i: argmax((idx[i, k], val[i, k]), axis=k), name="T") + +# 生成的 IR 代码: +s = te.create_schedule(T0.op) +print(tvm.lower(s, [idx, val, T0, T1], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(idx_1: handle, val_1: handle, T_2: handle, T_3: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {idx: Buffer(idx_2: Pointer(int32), int32, [(stride: int32*m: int32)], [], type="auto"), + val: Buffer(val_2: Pointer(int32), int32, [(stride_1: int32*m)], [], type="auto"), + T: Buffer(T_4: Pointer(int32), int32, [(stride_2: int32*m)], [], type="auto"), + T_1: Buffer(T_5: Pointer(int32), int32, [(stride_3: int32*m)], [], type="auto")} + buffer_map = {idx_1: idx, val_1: val, T_2: T, T_3: T_1} + preflattened_buffer_map = {idx_1: idx_3: Buffer(idx_2, int32, [m, n: int32], [stride, stride_4: int32], type="auto"), val_1: val_3: Buffer(val_2, int32, [m, n], [stride_1, stride_5: int32], type="auto"), T_2: T_6: Buffer(T_4, int32, [m], [stride_2], type="auto"), T_3: T_7: Buffer(T_5, int32, [m], [stride_3], type="auto")} { + for (i: int32, 0, m) { + T[(i*stride_2)] = -1 + T_1[(i*stride_3)] = -2147483648 + for (k: int32, 0, n) { + T[(i*stride_2)] = @tir.if_then_else((val[((i*stride_1) + (k*stride_5))] <= T_1[(i*stride_3)]), T[(i*stride_2)], idx[((i*stride) + (k*stride_4))], dtype=int32) + T_1[(i*stride_3)] = @tir.if_then_else((val[((i*stride_1) + (k*stride_5))] <= T_1[(i*stride_3)]), T_1[(i*stride_3)], val[((i*stride_1) + (k*stride_5))], dtype=int32) + } + } +} +``` + +:::note +若对归约不熟悉,可以参考 [定义通用交换归约运算](https://tvm.apache.org/docs/how_to/work_with_schedules/reduction.html#general-reduction)。 +::: + +## 使用元组输入调度操作 + +虽然一次 batch 操作会有多个输出,但它们只能一起调度。 + +``` python +n = te.var("n") +m = te.var("m") +A0 = te.placeholder((m, n), name="A0") +B0, B1 = te.compute((m, n), lambda i, j: (A0[i, j] + 2, A0[i, j] * 3), name="B") +A1 = te.placeholder((m, n), name="A1") +C = te.compute((m, n), lambda i, j: A1[i, j] + B0[i, j], name="C") + +s = te.create_schedule(C.op) +s[B0].compute_at(s[C], C.op.axis[0]) +# 生成的 IR 代码: +print(tvm.lower(s, [A0, A1, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A0_1: handle, A1_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A0: Buffer(A0_2: Pointer(float32), float32, [(stride: int32*m: int32)], [], type="auto"), + A1: Buffer(A1_2: Pointer(float32), float32, [(stride_1: int32*m)], [], type="auto"), + C: Buffer(C_2: Pointer(float32), float32, [(stride_2: int32*m)], [], type="auto")} + buffer_map = {A0_1: A0, A1_1: A1, C_1: C} + preflattened_buffer_map = {A0_1: A0_3: Buffer(A0_2, float32, [m, n: int32], [stride, stride_3: int32], type="auto"), A1_1: A1_3: Buffer(A1_2, float32, [m, n], [stride_1, stride_4: int32], type="auto"), C_1: C_3: Buffer(C_2, float32, [m, n], [stride_2, stride_5: int32], type="auto")} { + allocate(B.v0: Pointer(global float32), float32, [n]), storage_scope = global; + allocate(B.v1: Pointer(global float32), float32, [n]), storage_scope = global; + for (i: int32, 0, m) { + for (j: int32, 0, n) { + B.v0_1: Buffer(B.v0, float32, [n], [])[j] = (A0[((i*stride) + (j*stride_3))] + 2f32) + B.v1_1: Buffer(B.v1, float32, [n], [])[j] = (A0[((i*stride) + (j*stride_3))]*3f32) + } + for (j_1: int32, 0, n) { + C[((i*stride_2) + (j_1*stride_5))] = (A1[((i*stride_1) + (j_1*stride_4))] + B.v0_1[j_1]) + } + } +} +``` + +## 总结 + +本教程介绍元组输入操作的用法。 + +* 描述常规的批量计算。 +* 用元组输入描述归约操作。 +* 注意,只能根据操作而不是张量来调度计算。 + +[下载 Python 源代码:tuple_inputs.py](https://tvm.apache.org/docs/_downloads/68abf665197871646fffcd0955bddad7/tuple_inputs.py) + +[下载 Jupyter Notebook:tuple_inputs.ipynb](https://tvm.apache.org/docs/_downloads/a1417396e306d987107a7a39376ec261/tuple_inputs.ipynb) diff --git a/versioned_docs/version-0.12.0/how_to/te_schedules/08-tedd_visualization.md b/versioned_docs/version-0.12.0/how_to/te_schedules/08-tedd_visualization.md new file mode 100644 index 00000000..8397a91a --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/te_schedules/08-tedd_visualization.md @@ -0,0 +1,113 @@ +--- +title: 用 TEDD 进行可视化 +--- + +# 用 TEDD 进行可视化 + +:::note +单击 [此处](https://tvm.apache.org/docs/how_to/work_with_schedules/tedd.html#sphx-glr-download-how-to-work-with-schedules-tedd-py) 下载完整的示例代码 +::: + +**作者**:[Yongfeng Gu](https://github.com/yongfeng-nv) + +本文介绍使用 TEDD(Tensor Expression Debug Display)对张量表达式进行可视化。 + +张量表达式使用原语进行调度,单个原语容易理解,但组合在一起时,就会变得复杂。在张量表达式中引入了调度原语的操作模型。 + +* 不同调度原语之间的交互, +* 调度原语对最终代码生成的影响。 + +操作模型基于数据流图、调度树和 IterVar 关系图。调度原语在这些计算图上进行操作。 + +TEDD 从给定的 schedule 中呈现这三个计算图,本教程演示了如何用 TEDD,以及如何解释渲染的计算图。 + +``` python +import tvm +from tvm import te +from tvm import topi +from tvm.contrib import tedd +``` + +## 使用 Bias 和 ReLU 定义和调度卷积 + +用 Bias 和 ReLU 为卷积构建一个张量表达式示例,首先连接 conv2d、add 和 relu TOPIs,然后创建一个 TOPI 通用 schedule。 + +``` python +batch = 1 +in_channel = 256 +in_size = 32 +num_filter = 256 +kernel = 3 +stride = 1 +padding = "SAME" +dilation = 1 + +A = te.placeholder((in_size, in_size, in_channel, batch), name="A") +W = te.placeholder((kernel, kernel, in_channel, num_filter), name="W") +B = te.placeholder((1, num_filter, 1), name="bias") + +with tvm.target.Target("llvm"): + t_conv = topi.nn.conv2d_hwcn(A, W, stride, padding, dilation) + t_bias = topi.add(t_conv, B) + t_relu = topi.nn.relu(t_bias) + s = topi.generic.schedule_conv2d_hwcn([t_relu]) +``` + +## 使用 TEDD 渲染计算图 + +通过渲染计算图来查看计算及其调度方式。若在 Jupyter Notebook 中运行本教程,则可以用以下注释行来渲染 SVG 图形,让它直接在 Notebook 中显示。 + +``` python +tedd.viz_dataflow_graph(s, dot_file_path="/tmp/dfg.dot") +# tedd.viz_dataflow_graph(s, show_svg = True) +``` + + ![图片](https://github.com/dmlc/web-data/raw/main/tvm/tutorial/tedd_dfg.png) + +第一个是数据流图。每个节点代表一个阶段,中间是名称和内存范围,两边是输入/输出信息。图中的边显示节点的依赖关系。 + +``` python +tedd.viz_schedule_tree(s, dot_file_path="/tmp/scheduletree.dot") +# tedd.viz_schedule_tree(s, show_svg = True) +``` + +上面渲染了调度树图。注意范围不可用的警告,它表明要调用 normalize() 来推断范围信息。跳过检查第一个调度树,推荐通过比较 normalize() 之前和之后的计算图来了解其影响。 + +``` python +s = s.normalize() +tedd.viz_schedule_tree(s, dot_file_path="/tmp/scheduletree2.dot") +# tedd.viz_schedule_tree(s, show_svg = True) +``` + + ![图片](https://github.com/dmlc/web-data/raw/main/tvm/tutorial/tedd_st.png) + +仔细看第二个调度树,ROOT 下的每一个 block 代表一个阶段。阶段名称显示在顶行,计算显示在底行。中间行是 IterVars,外部越高,内部越低。 + +IterVar 行包含其索引、名称、类型和其他可选信息。以 W.shared 阶段为例,第一行是名称「W.shared」和内存范围「Shared」。它的计算是 `W(ax0, ax1, ax2, ax3)`。最外层循环 IterVar 是 ax0.ax1.fused.ax2.fused.ax3.fused.outer,以 kDataPar 的 0 为索引,绑定到 threadIdx.y,范围(min=0,ext=8)。 + +还可以用索引框的颜色来判断 IterVar 类型,如图所示。 + +如果一个阶段在任何其他阶段都没有计算,则它有直接到根节点的边;否则,它有一条边指向它所附加的 IterVar,例如 W.shared 在中间计算阶段附加到 rx.outer。 + +:::note +根据定义,IterVars 是内部节点,而计算是调度树中的叶节点。为提高可读性,省略了 IterVars 之间的边和一个阶段内的计算,使每个阶段都是一个块。 +::: + +``` python +tedd.viz_itervar_relationship_graph(s, dot_file_path="/tmp/itervar.dot") +# tedd.viz_itervar_relationship_graph(s, show_svg = True) +``` + + ![图片](https://github.com/dmlc/web-data/raw/main/tvm/tutorial/tedd_itervar_rel.png) + +最后一个是 IterVar 关系图。每个子图代表一个阶段,包含 IterVar 节点和转换节点。 + +例如,W.shared 有三个拆分节点和三个融合节点。其余的是与调度树中的 IterVar 行格式相同的 IterVar 节点。 Root IterVars 是那些不受任何变换节点驱动的,例如 ax0;叶节点 IterVars 不驱动任何转换节点,并且具有非负索引,例如索引为 0 的 ax0.ax1.fused.ax2.fused.ax3.fused.outer。 + +## 总结 + +本教程演示 TEDD 的用法。用一个 TOPI 构建的示例来显示底层的 schedule,可在任何调度原语之前和之后用它来检查其效果。 + +[下载 Python 源代码:tedd.py](https://tvm.apache.org/docs/_downloads/c253040abc62eace272e406b7e1a4df5/tedd.py) + +[下载 Jupyter Notebook:tedd.ipynb](https://tvm.apache.org/docs/_downloads/a7aff5918e1b86809a5bd1da8bef7229/tedd.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/how_to/te_schedules/_category_.json b/versioned_docs/version-0.12.0/how_to/te_schedules/_category_.json new file mode 100644 index 00000000..adf9e585 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/te_schedules/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 140 +} diff --git a/versioned_docs/version-0.12.0/how_to/te_schedules/index.md b/versioned_docs/version-0.12.0/how_to/te_schedules/index.md new file mode 100644 index 00000000..f8816836 --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to/te_schedules/index.md @@ -0,0 +1,12 @@ +--- +title: 使用张量表达式和 Schedule +--- + +* [TVM 中的 schedule 原语](primitive) +* [归约](reduction) +* [内联及数学函数](math) +* [扫描和循环内核](scan_recurrent) +* [外部张量函数](external_tensor) +* [使用 Tensorize 来利用硬件内联函数](tensorize) +* [使用输入的元组进行计算和归约](compute_reduce) +* [使用张量表达式调试显示(TEDD)进行可视化](tedd_visualization) diff --git a/versioned_docs/version-0.12.0/how_to_idx.md b/versioned_docs/version-0.12.0/how_to_idx.md new file mode 100644 index 00000000..047de16b --- /dev/null +++ b/versioned_docs/version-0.12.0/how_to_idx.md @@ -0,0 +1,20 @@ +--- +title: 常见问题 +slug: how_to +sidebar_position: 4 +--- + +本章旨在回答具体问题,例如「如何编译模型?」或「如何用张量表达式优化 schedule?」 + +* [编译深度学习模型](/docs/how_to/compile) +* [部署模型并与 TVM 集成](/docs/how_to/deploy) +* [使用 Relay](/docs/how_to/relay) +* [使用张量表达式和 schedule](/docs/how_to/te_schedules) +* [优化张量算子](/docs/how_to/optimize) +* [使用模板和 AutoTVM 自动调优](/docs/how_to/autotune) +* [使用 AutoScheduler 进行无模板调度](/docs/how_to/autoscheduler) +* [使用 microTVM](/docs/how_to/microtvm) +* [扩展 TVM](/docs/how_to/extend) +* [Profile Models](/docs/how_to/models) +* [处理 TVM 错误信息](/docs/how_to/errors) +* [FAQ](/docs/how_to/FAQ) diff --git a/versioned_docs/version-0.12.0/index.md b/versioned_docs/version-0.12.0/index.md new file mode 100644 index 00000000..e8177c68 --- /dev/null +++ b/versioned_docs/version-0.12.0/index.md @@ -0,0 +1,17 @@ +--- +title: 欢迎查看 Apache TVM 中文文档! +sidebar_label: 欢迎 +sidebar_position: 0 +--- + +TVM 是一个开源的深度学习编译器,适用于 CPU、GPU、ARM 等多种硬件架构,旨在使机器学习工程师能够在任意硬件后端,高效地运行并优化计算。 + +鉴于 TVM 相关的中文学习资料比较零散,不利于开发者系统性学习,我们在 GitHub 上创建了 TVM 文档翻译项目。 + +目前中文文档的版本是基于 TVM v0.10.0 进行的本土化,随着 TVM 官方版本及文档的更新,中文文档也会不断调整,你可以: + +* 学习 TVM 中文文档,为翻译不准确或有歧义的地方提交 [Issue](https://github.com/hyperai/tvm-cn/issues) 或 [PR](https://github.com/hyperai/tvm-cn/pulls) +* 参与开源共建、追踪文档更新,并认领文档翻译,成为 TVM 中文文档贡献者 +* 加入 TVM 中文社区、结识同行并参与更多讨论及交流 + +现在,让我们开启 TVM 官方文档的学习之旅吧! diff --git a/versioned_docs/version-0.12.0/install-idx.md b/versioned_docs/version-0.12.0/install-idx.md new file mode 100644 index 00000000..6b225c33 --- /dev/null +++ b/versioned_docs/version-0.12.0/install-idx.md @@ -0,0 +1,23 @@ +--- +title: 安装 TVM +slug: /install +sidebar_position: 0 +--- + +* [从源码安装](/docs/install/from_source) +* [Docker 镜像](/docs/install/docker) +* [NNPACK Contrib 安装](/docs/install/nnpack) + +查看 [从源码安装](/docs/install/from_source)。 从源码安装 TVM +可以最大限度地灵活配置和编译。如果您对部署到移动端或嵌入式设备感兴趣, +则无需在设备上安装整个 TVM 编译栈,只需利用 runtime,并通过 +[部署模型与 TVM 集成](/docs/how_to/deploy) 安装即可。 + +想要快速运行 TVM 或访问其他 Demo 和教程,请查看 +[Docker 镜像](/docs/install/docker)。同时可以通过 `pip` 在本地使用。 + +``` bash +pip install apache-tvm +``` + +更多详细信息,可以查看 [tlcpack.ai](https://tlcpack.ai/) diff --git a/versioned_docs/version-0.12.0/install/_category_.json b/versioned_docs/version-0.12.0/install/_category_.json new file mode 100644 index 00000000..3fca6fb9 --- /dev/null +++ b/versioned_docs/version-0.12.0/install/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 0 +} diff --git a/versioned_docs/version-0.12.0/install/docker.md b/versioned_docs/version-0.12.0/install/docker.md new file mode 100644 index 00000000..bb2a6ff0 --- /dev/null +++ b/versioned_docs/version-0.12.0/install/docker.md @@ -0,0 +1,68 @@ +--- +title: Docker 镜像 +sidebar_position: 2 +--- + +开发者可以利用 Docker 工具脚本,建立开发环境。这也有助于运行 TVM Demo +和教程。需要用到 +[Docker](https://docs.docker.com/engine/installation/), +如果使用 CUDA 则需要 +[nvidia-docker](https://github.com/NVIDIA/nvidia-docker/)。 + +获取 TVM 源码发行版或克隆 GitHub 仓库,以获取辅助脚本: + +``` bash +git clone --recursive https://github.com/apache/tvm tvm +``` + +使用以下命令来启动 Docker 镜像: + +``` bash +/path/to/tvm/docker/bash.sh +``` + +完成本地构建后,这里的 image-name 可以是一个本地的 Docker +镜像名称,例如:`tvm.ci_cpu`。 + +该辅助脚本可实现: + +- 挂载当前目录到 /workspace +- 将用户切换为调用 bash.sh 的用户(这样您就可以读/写主机系统) +- 在 Linux 上使用宿主机的网络。由于无法支持主机网络驱动器,请在 macOS + 上使用桥接网络并暴露 8888 端口,以使用 Jupyter Notebook。 + +输入以下内容启动 Jupyter Notebook: + +``` bash +jupyter notebook +``` + +如果你在 macOS 上启动 Jupyter Notebook 时看到报错 +`OSError: [Errno 99] Cannot assign requested address`,可通过以下方式改变绑定的 +IP 地址: + +``` bash +jupyter notebook --ip=0.0.0.0 +``` + +注意,在 macOS 上,由于我们使用桥接网络,Jupyter Notebook +将被报告在一个类似于 `http://{container_hostname}:8888/?token=...` 的 +URL 上运行。 在浏览器中粘贴时,需把 container_hostname 替换为 +`localhost`。 + +## Docker 源代码 + +查看 [Docker +源代码](https://github.com/apache/tvm/tree/main/docker),构建自己的 +Docker 镜像。 + +运行以下命令来构建 Docker 镜像: + +``` bash +/path/to/tvm/docker/build.sh +``` + +你也可以利用非官方的第三方预建镜像,注意:这些镜像是用来测试的,并不是 +ASF 的版本。 + +[https://hub.docker.com/r/tlcpack/](https://hub.docker.com/r/tlcpack/). diff --git a/versioned_docs/version-0.12.0/install/from_source.md b/versioned_docs/version-0.12.0/install/from_source.md new file mode 100644 index 00000000..24817f72 --- /dev/null +++ b/versioned_docs/version-0.12.0/install/from_source.md @@ -0,0 +1,344 @@ +--- +title: 从源码安装 +sidebar_position: 1 +--- + +在各种系统中从 0 到 1 构建和安装 TVM 软件包包括两个步骤: + +1. 从 C++ 代码中构建共享库(Linux:`libtvm.so`;macOS + :`libtvm.dylib`;Windows:`libtvm.dll`)。 +2. 为编程语言包进行设置(如 Python 包)。 + +下载 TVM 源代码,请访问 [下载页面](https://tvm.apache.org/download)。 + +## 开发者:从 GitHub 获取源代码 + +从 GitHub 上克隆源码仓库,请使用 `--recursive` 选项来克隆子模块。 + +``` bash +git clone --recursive https://github.com/apache/tvm tvm +``` + +Windows 用户可以打开 Git shell,并输入以下命令: + +``` bash +git submodule init +git submodule update +``` + +## 构建共享库 + +我们的目标是构建共享库: + +* 在 Linux 上,目标库是 *libtvm.so* 和 *libtvm_runtime.so* +* 在 MacOS 上,目标库是 *libtvm.dylib* 和 *libtvm_runtime.dylib* +* 在 Windows 上,目标库是 *libtvm.dll* 和 *libtvm_runtime.dll* + +也可以只 [构建运行时库](/docs/how_to/deploy)。 + +`TVM` 库的最低构建要求是: + +* 支持 C++17 的最新 C++ 编译器 + * GCC 7.1 + * Clang 5.0 + * Apple Clang 9.3 + * Visual Stuio 2019 (v16.7) +* CMake 3.18 或更高版本 +* 推荐使用 LLVM 构建 TVM 库以启用所有功能。 +* 如需使用 CUDA,请确保 CUDA 工具包的版本至少在 8.0 以上。注意:CUDA 旧版本升级后,请删除旧版本并重新启动。 +* macOS 可安装 [Homebrew](https://brew.sh) 以方便安装和管理依赖。 +* Python:推荐使用 3.7.X+ 和 3.8.X+ 版本,3.9.X+ 暂时[不支持](https://github.com/apache/tvm/issues/8577)。 + +在 Ubuntu/Debian 等 Linux +操作系统上,要安装这些依赖环境,请在终端执行: + +``` bash +sudo apt-get update +sudo apt-get install -y python3 python3-dev python3-setuptools gcc libtinfo-dev zlib1g-dev build-essential cmake libedit-dev libxml2-dev +``` +需要注意的是,apt中的CMake版本可能不够新,需要直接从[Kitware的第三方apt仓](https://apt.kitware.com)进行安装。 + +在 Fedora/CentOS 等相关的操作系统上,使用如下命令: +``` bash +sudo dnf update +sudo dnf groupinstall -y "Development Tools" +sudo dnf install -y python-devel ncurses-compat-libs zlib-devel cmake libedit-devel libxml2-devel +``` + +用 Homebrew 为搭载 Intel 或 M1 芯片的 macOS 安装所需的依赖,需遵循 +Homebrew 指定的安装步骤,以保证正确安装和配置这些依赖: + +``` bash +brew install gcc git cmake +brew install llvm +brew install python@3.8 +``` + +使用 cmake 来构建库。TVM 的配置可以通过编辑 config.cmake +和/或在命令行传递 cmake flags 来修改: + +- 如果没有安装 cmake,可访问 [官方网站](https://cmake.org/download/) + 下载最新版本。 + +- 创建一个构建目录,将 `cmake/config.cmake` 复制到该目录。 + + ``` bash + mkdir build + cp cmake/config.cmake build + ``` + +- 编辑 `build/config.cmake` 自定义编译选项 + + - 对于 macOS 某些版本的 Xcode,需要在 LDFLAGS 中添加 + `-lc++abi`,以免出现链接错误。 + + - 将 `set(USE_CUDA OFF)` 改为 `set(USE_CUDA ON)` 以启用 CUDA + 后端。对其他你想构建的后端和库(OpenCL,RCOM,METAL,VULKAN\...\...)做同样的处理。 + + - 为了便于调试,请确保使用 `set(USE_GRAPH_EXECUTOR ON)` 和 + `set(USE_PROFILER ON)` 启用嵌入式图形执行器(embedded graph + executor)和调试功能。 + + - 如需用 IR 调试,可以设置 `set(USE_RELAY_DEBUG ON)`,同时设置环境变量 *TVM_LOG_DEBUG*。 + + > ``` bash + > export TVM_LOG_DEBUG="ir/transform.cc=1;relay/ir/transform.cc=1" + > ``` + +- TVM 需要 LLVM 用于 CPU 代码生成工具(Codegen)。推荐使用 LLVM 构建。 + + - 使用 LLVM 构建时需要 LLVM 4.0 或更高版本。注意,默认的 apt 中的 + LLVM 版本可能低于 4.0。 + - 由于 LLVM 从源码构建需要很长时间,推荐从 [LLVM + 下载页面](http://releases.llvm.org/download.html) + 下载预构建版本。 + - 解压缩到某个特定位置,修改 `build/config.cmake` 以添加 + `set(USE_LLVM /path/to/your/llvm/bin/llvm-config)` + - 或直接设置 `set(USE_LLVM ON)`,利用 CMake 搜索一个可用的 + LLVM 版本。 + - 也可以使用 [LLVM Ubuntu 每日构建](https://apt.llvm.org/) + - 注意 apt-package 会在 `llvm-config` + 中附加版本号。例如,如果你安装了 LLVM 10 版本,则设置 + `set(USE_LLVM llvm-config-10)` + - PyTorch 的用户建议设置 + `set(USE_LLVM "/path/to/llvm-config --link-static")` 和 + `set(HIDE_PRIVATE_SYMBOLS ON)` 以避免 TVM 和 PyTorch + 使用的不同版本的 LLVM 之间潜在的符号冲突。 + - 某些支持平台上,[Ccache 编译器 Wrapper](https://ccache.dev/) + 可帮助减少 TVM 的构建时间。在 TVM 构建中启用 CCache 的方法包括: + - 在`build/config.cmake`中设置`USE_CCACHE=AUTO`。如果装有CCache,便会被使用。 + - Ccache 的 Masquerade 模式。通常在 Ccache + 安装过程中启用。要让 TVM 在 masquerade 中使用 + Ccache,只需在配置 TVM 的构建系统时指定适当的 C/C++ + 编译器路径。例如:`cmake -DCMAKE_CXX_COMPILER=/usr/lib/ccache/c++ ...`。 + - Ccache 作为 CMake 的 C++ 编译器前缀。在配置 TVM + 的构建系统时,将 CMake 变量 `CMAKE_CXX_COMPILER_LAUNCHER` + 设置为一个合适的值,例如,`cmake -DCMAKE_CXX_COMPILER_LAUNCHER=ccache ...`。 + +- 构建 TVM 及相关库: + + ``` bash + cd build + cmake .. + make -j4 + ``` + + - 可以使用 Ninja 来加速构建 + + ``` bash + cd build + cmake .. -G Ninja + ninja + ``` + + - 在 TVM 的根目录下也有一个 + Makefile,它可以自动完成其中的几个步骤:创建构建目录,将默认的 + `config.cmake` 复制到该构建目录下,运行 cmake,并运行 make。 + + 构建目录可以用环境变量 `TVM_BUILD_PATH` 来指定。如果 + `TVM_BUILD_PATH` 没有设置,Makefile 就会假定应该使用 TVM 里面的 + `build` 目录。 由 `TVM_BUILD_PATH` + 指定的路径可以是绝对路径,也可以是相对于 TVM 根目录的路径。 如果 + `TVM_BUILD_PATH` + 被设置为一个以空格分隔的路径列表,则将创建所有列出的路径。 + + 如果使用另一个构建目录,那么应该在运行时设置环境变量 + `TVM_LIBRARY_PATH`,它指向编译后的 `libtvm.so` 和 + `libtvm_runtime.so` 的位置。 如果没有设置,TVM 将寻找相对于 TVM + Python 模块的位置。与 `TVM_BUILD_PATH` + 不同,这必须是一个绝对路径。 + + ``` bash + # 在 "build" 目录下构建 + make + + # 替代位置,"build_debug" + TVM_BUILD_PATH=build_debug make + + # 同时构建 "build_release" 和 "build_debug" + TVM_BUILD_PATH="build_debug build_release" make + + # 使用调试构建 + TVM_LIBRARY_PATH=~/tvm/build_debug python3 + ``` + +如果一切顺利,我们就可以去查看 [Python 包的安装](#python-package-installation) 了。 + +### 使用 Conda 环境进行构建 + +Conda 可以用来获取运行 TVM 所需的必要依赖。如果没有安装 Conda,请参照 +[Conda +安装指南](https://docs.conda.io/projects/conda/en/latest/user-guide/install/) +来安装 Miniconda 或 Anaconda。在 Conda 环境中运行以下命令: + +``` bash +# 用 yaml 指定的依赖创建 Conda 环境 +conda env create --file conda/build-environment.yaml +# 激活所创建的环境 +conda activate tvm-build +``` + +上述命令将安装所有必要的构建依赖,如 CMake 和 +LLVM。接下来可以运行上一节中的标准构建过程。 + +在 Conda 环境之外使用已编译的二进制文件,可将 LLVM 设置为静态链接模式 +`set(USE_LLVM "llvm-config --link-static")`。 +这样一来,生成的库就不会依赖于 Conda 环境中的动态 LLVM 库。 + +以上内容展示了如何使用 Conda 提供必要的依赖,从而构建 +libtvm。如果已经使用 Conda 作为软件包管理器,并且希望直接将 TVM 作为 +Conda 软件包来构建和安装,可以按照以下指导进行: + +``` bash +conda build --output-folder=conda/pkg conda/recipe +# 在启用 CUDA 的情况下运行 conda/build_cuda.sh 来构建 +conda install tvm -c ./conda/pkg +``` + +### 在 Windows 上构建 + +TVM 支持通过 MSVC 使用 CMake 构建。需要有一个 Visual Studio 编译器。 VS +的最低版本为 **Visual Studio Enterprise 2019**(注意:查看针对 GitHub +Actions 的完整测试细节,请访问 [Windows 2019 +Runner](https://github.com/actions/virtual-environments/blob/main/images/win/Windows2019-Readme.md)。 +官方推荐 [使用 Conda 环境进行构建](#build-with-conda),以获取必要的依赖及激活的 tvm-build 环境。)运行以下命令行: + +``` bash +mkdir build +cd build +cmake -A x64 -Thost=x64 .. +cd .. +``` + +上述命令在构建目录下生成了解决方案文件。接着运行: + +``` bash +cmake --build build --config Release -- /m +``` + +### 构建 ROCm 支持 + +目前,ROCm 只在 Linux 上支持,因此所有教程均以 Linux 为基础编写的。 - +设置 `set(USE_ROCM ON)`,将 ROCM_PATH 设置为正确的路径。 - 需要先从 ROCm +中安装 HIP runtime。确保安装系统中已经安装了 ROCm。 - 安装 LLVM +的最新稳定版本(v6.0.1),以及 LLD,确保 `ld.lld` 可以通过命令行获取。 + +## Python 包的安装 + +### TVM 包 + +本部分介绍利用 `virtualenv` 或 `conda` 等虚拟环境和软件包管理器,来管理 +Python 软件包和依赖的方法。 + +Python 包位于 *tvm/python*。安装方法有两种: + +* 方法1 + + 本方法适用于有可能修改**代码的开发者**。 + + 设置环境变量 *PYTHONPATH*,告诉 Python 在哪里可以找到这个库。例如,假设我们在 */path/to/tvm* 目录下克隆了 *tvm*,我们可以在 *~/.bashrc* 中添加以下代码:这使得拉取代码及重建项目时,无需再次调用 `setup`,这些变化就会立即反映出来 + + ``` bash + export TVM_HOME=/path/to/tvm + export PYTHONPATH=$TVM_HOME/python:${PYTHONPATH} + ``` + +* 方法2 + + 通过 *setup.py* 安装 TVM 的 Python 绑定: + + ``` bash + # 为当前用户安装 TVM 软件包 + # 注意:如果你通过 homebrew 安装了 Python,那么在安装过程中就不需要 --user + # 它将被自动安装到你的用户目录下。 + # 在这种情况下,提供 --user 标志可能会在安装时引发错误。 + export MACOSX_DEPLOYMENT_TARGET=10.9 # 这是 mac 所需要的,以避免与 libstdc++ 的符号冲突 + cd python; python setup.py install --user; cd .. + ``` + +### Python 依赖 + +注意,如果你想要安装到一个受管理的本地环境,如 `virtualenv`,则不需要 +`--user` 标志。 + +- 必要的依赖: + +``` bash +pip3 install --user numpy decorator attrs +``` + +- 如果你想使用``tvmc``: TVM的命令行驱动: + +``` bash +pip3 install --user typing-extensions psutil scipy +``` + +- 使用 RPC 跟踪器 + +``` bash +pip3 install --user tornado +``` + +- 使用 auto-tuning 模块 + +``` bash +pip3 install --user tornado psutil 'xgboost>=1.1.0' cloudpickle +``` + +注意:在搭载 M1 芯片的 Mac 上,安装 xgboost / scipy +时可能遇到一些问题。scipy 和 xgboost 需要安装 openblas +等额外依赖。运行以下命令行,安装 scipy 和 xgboost 以及所需的依赖和配置: + +``` bash +brew install openblas gfortran + +pip install pybind11 cython pythran + +export OPENBLAS=/opt/homebrew/opt/openblas/lib/ + +pip install scipy --no-use-pep517 + +pip install 'xgboost>=1.1.0' +``` + +## 安装 Contrib 库 + +[NNPACK Contrib 安装](nnpack) + +## 启用 C++ 测试 + +可以用 [Google Test](https://github.com/google/googletest) 来驱动 TVM +中的 C++ 测试。安装 GTest 最简单的方法是从源代码安装: + +``` bash +git clone https://github.com/google/googletest +cd googletest +mkdir build +cd build +cmake -DBUILD_SHARED_LIBS=ON .. +make +sudo make install +``` + +安装成功后,可以用 `./tests/scripts/task_cpp_unittest.sh` 来构建和启动 +C++ 测试,或者直接用 `make cpptest` 构建。 diff --git a/versioned_docs/version-0.12.0/install/nnpack.md b/versioned_docs/version-0.12.0/install/nnpack.md new file mode 100644 index 00000000..93ea23eb --- /dev/null +++ b/versioned_docs/version-0.12.0/install/nnpack.md @@ -0,0 +1,82 @@ +--- +title: NNPACK Contrib 安装 +sidebar_position: 3 +--- + +# NNPACK Contrib 安装 + +[NNPACK](https://github.com/Maratyszcza/NNPACK) 是用于神经网络计算的加速包,可以在 x86-64、ARMv7 或 ARM64 架构的 CPU 上运行。使用 NNPACK,像 MXNet 这样的高级库可以加快多核 CPU 计算机(包括笔记本电脑和移动设备)上的执行速度。 + +:::note +由于 TVM 已经有原生调整的调度,这里的 NNPACK 主要是为了参考和比较。对于常规使用,原生调整的 TVM 实现更佳。 +::: + +TVM 支持 NNPACK 在卷积、最大池和全连接层中进行前向传播(仅限推理)。在本文档中,我们对如何将 NNPACK 与 TVM 一起使用进行了高级概述。 + +## 条件 + +NNPACK 的底层实现使用了多种加速方法,包括 fft 和 winograd。这些算法在某些特殊的批处理大小、内核大小和步幅设置上比其他算法效果更好,因此根据上下文,并非所有卷积、最大池或全连接层都可以由 NNPACK 提供支持。NNPACK 仅支持 Linux 和 OS X 系统,目前不支持 Windows。 + +## 构建/安装 NNPACK + +如果训练后的模型满足使用 NNPACK 的一些条件,则可以构建支持 NNPACK 的 TVM。请按照以下简单步骤操作: + +使用以下命令构建 NNPACK 共享库。 TVM 会动态链接 NNPACK。 + +注意:以下 NNPACK 安装指导已经在 Ubuntu 16.04 上进行了测试。 + +### 构建 Ninja + +NNPACK 需要最新版本的 Ninja。所以我们需要从源代码安装 ninja。 + +``` bash +git clone git://github.com/ninja-build/ninja.git +cd ninja +./configure.py --bootstrap +``` + +设置环境变量 PATH 以告诉 bash 在哪里可以找到 ninja 可执行文件。例如,假设我们在主目录 \~ 上克隆了 ninja。然后我们可以在 \~/.bashrc 中添加以下行。 + +``` bash +export PATH="${PATH}:~/ninja" +``` + +### 构建 NNPACK + +CMAKE 新版 NNPACK 单独下载 [Peach](https://github.com/Maratyszcza/PeachPy) 等依赖 + +注意:至少在 OS X 上,运行下面的 ninja install 会覆盖安装在 /usr/local/lib 中的 googletest 库。如果您再次构建 googletest 以替换 nnpack 副本,请务必将 -DBUILD_SHARED_LIBS=ON 传给 cmake。 + +``` bash +git clone --recursive https://github.com/Maratyszcza/NNPACK.git +cd NNPACK + +# 在 CFLAG 和 CXXFLAG 中添加 PIC 选项以构建 NNPACK 共享库 +sed -i "s|gnu99|gnu99 -fPIC|g" CMakeLists.txt +sed -i "s|gnu++11|gnu++11 -fPIC|g" CMakeLists.txt +mkdir build +cd build + +# 生成 ninja 构建规则并在配置中添加共享库 +cmake -G Ninja -D BUILD_SHARED_LIBS=ON .. +ninja +sudo ninja install + +# 在你的 ldconfig 中添加 NNPACK 的 lib 文件夹 +echo "/usr/local/lib" > /etc/ld.so.conf.d/nnpack.conf +sudo ldconfig +``` + +## 构建支持 NNPACK 的 TVM + +``` bash +git clone --recursive https://github.com/apache/tvm tvm +``` + +* 在 config.cmake 中设置 *set(USE_NNPACK ON)*。 +* 将 *NNPACK_PATH* 设置为 $(YOUR_NNPACK_INSTALL_PATH) + 配置后使用 make 构建 TVM + +``` bash +make +``` \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/reference/_category_.json b/versioned_docs/version-0.12.0/reference/_category_.json new file mode 100644 index 00000000..48076f83 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Reference Guide", + "position": 700 +} diff --git a/versioned_docs/version-0.12.0/reference/api/links.md b/versioned_docs/version-0.12.0/reference/api/links.md new file mode 100644 index 00000000..2edb4aa3 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/links.md @@ -0,0 +1,10 @@ +--- +title: Other APIs +--- + +This page contains links to API references that are built with different +doc build system. + +- [C++ doyxgen API](https://tvm.apache.org/docs/reference/api/doxygen/index.html) +- [Typescript typedoc API](https://tvm.apache.org/docs/reference/api/typedoc/index.html) +- [Java Javadoc API](https://tvm.apache.org/docs/reference/api/javadoc/index.html) diff --git a/versioned_docs/version-0.12.0/reference/api/python/auto_scheduler.md b/versioned_docs/version-0.12.0/reference/api/python/auto_scheduler.md new file mode 100644 index 00000000..2c7bf16f --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/auto_scheduler.md @@ -0,0 +1,7 @@ +--- +title: tvm.auto_scheduler +--- + +::: +tvm.auto_scheduler +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/autotvm.md b/versioned_docs/version-0.12.0/reference/api/python/autotvm.md new file mode 100644 index 00000000..e6319ab4 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/autotvm.md @@ -0,0 +1,103 @@ +--- +title: tvm.autotvm +--- + +::: automodule +tvm.autotvm +::: + +::: autofunction +tvm.autotvm.apply_history_best +::: + +## tvm.autotvm.measure + +::: automodule +tvm.autotvm.measure.measure +::: + +::: +tvm.autotvm.measure.MeasureInput +::: + +::: +tvm.autotvm.measure.MeasureResult +::: + +::: autofunction +tvm.autotvm.measure.measure_option +::: + +::: autofunction +tvm.autotvm.measure.create_measure_batch +::: + +::: autoclass +tvm.autotvm.measure.measure_methods.LocalBuilder +::: + +::: autoclass +tvm.autotvm.measure.measure_methods.RPCRunner +::: + +::: autoclass +tvm.autotvm.measure.measure_methods.LocalRunner +::: + +## tvm.autotvm.tuner + +::: +tvm.autotvm.tuner +::: + +::: +tvm.autotvm.tuner.Tuner +::: + +::: +tvm.autotvm.tuner.RandomTuner +::: + +::: +tvm.autotvm.tuner.GridSearchTuner +::: + +::: +tvm.autotvm.tuner.GATuner +::: + +::: +tvm.autotvm.tuner.XGBTuner +::: + +::: +tvm.autotvm.tuner.callback +::: + +## tvm.autotvm.task + +::: +tvm.autotvm.task +::: + +::: +tvm.autotvm.task.task +::: + +::: +tvm.autotvm.task.space +::: + +::: +tvm.autotvm.task.dispatcher +::: + +::: +tvm.autotvm.task.topi_integration +::: + +## tvm.autotvm.record + +::: +tvm.autotvm.record +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/contrib.md b/versioned_docs/version-0.12.0/reference/api/python/contrib.md new file mode 100644 index 00000000..c0b574b0 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/contrib.md @@ -0,0 +1,145 @@ +--- +title: tvm.contrib +--- + +::: automodule +tvm.contrib +::: + +## tvm.contrib.cblas + +::: +tvm.contrib.cblas +::: + +## tvm.contrib.clang + +::: +tvm.contrib.clang +::: + +## tvm.contrib.cc + +::: +tvm.contrib.cc +::: + +## tvm.contrib.cublas + +::: +tvm.contrib.cublas +::: + +## tvm.contrib.dlpack + +::: +tvm.contrib.dlpack +::: + +## tvm.contrib.emcc + +::: +tvm.contrib.emcc +::: + +## tvm.contrib.miopen + +::: +tvm.contrib.miopen +::: + +## tvm.contrib.mxnet + +::: +tvm.contrib.mxnet +::: + +## tvm.contrib.ndk + +::: +tvm.contrib.ndk +::: + +## tvm.contrib.nnpack + +::: +tvm.contrib.nnpack +::: + +## tvm.contrib.nvcc + +::: +tvm.contrib.nvcc +::: + +## tvm.contrib.pickle_memoize + +::: +tvm.contrib.pickle_memoize +::: + +## tvm.contrib.random + +::: +tvm.contrib.random +::: + +## tvm.contrib.relay_viz + +::: +tvm.contrib.relay_viz +::: + +::: +tvm.contrib.relay_viz.dot +::: + +::: +tvm.contrib.relay_viz.terminal +::: + +::: +tvm.contrib.relay_viz.interface +::: + +## tvm.contrib.rocblas + +::: +tvm.contrib.rocblas +::: + +## tvm.contrib.rocm + +::: +tvm.contrib.rocm +::: + +## tvm.contrib.sparse + +::: +tvm.contrib.sparse +::: + +## tvm.contrib.spirv + +::: +tvm.contrib.spirv +::: + +## tvm.contrib.tar + +::: +tvm.contrib.tar +::: + +## tvm.contrib.utils + +::: +tvm.contrib.utils +::: + +## tvm.contrib.xcode + +::: +tvm.contrib.xcode +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/driver.md b/versioned_docs/version-0.12.0/reference/api/python/driver.md new file mode 100644 index 00000000..4cb6293a --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/driver.md @@ -0,0 +1,15 @@ +--- +title: tvm.driver +--- + +::: automodule +tvm.driver +::: + +::: autofunction +tvm.lower +::: + +::: autofunction +tvm.build +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/error.md b/versioned_docs/version-0.12.0/reference/api/python/error.md new file mode 100644 index 00000000..ba83dfd5 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/error.md @@ -0,0 +1,7 @@ +--- +title: tvm.error +--- + +::: +tvm.error +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/graph_executor.md b/versioned_docs/version-0.12.0/reference/api/python/graph_executor.md new file mode 100644 index 00000000..90110e9b --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/graph_executor.md @@ -0,0 +1,7 @@ +--- +title: tvm.contrib.graph_executor +--- + +::: +tvm.contrib.graph_executor +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/index.md b/versioned_docs/version-0.12.0/reference/api/python/index.md new file mode 100644 index 00000000..ab82bf8d --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/index.md @@ -0,0 +1,10 @@ +--- +title: Python API +--- + +``` +runtime ndarray error ir target tir te driver relay/index relay/frontend +relay/nn relay/vision relay/image relay/transform relay/analysis +relay/backend relay/dataflow_pattern relay/testing autotvm +auto_scheduler rpc micro contrib graph_executor topi vta/index +``` diff --git a/versioned_docs/version-0.12.0/reference/api/python/ir.md b/versioned_docs/version-0.12.0/reference/api/python/ir.md new file mode 100644 index 00000000..16eebd04 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/ir.md @@ -0,0 +1,17 @@ +## tvm.ir + +::: +tvm.ir +::: + +## tvm.instrument + +::: +tvm.instrument +::: + +## tvm.transform + +::: +tvm.transform +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/micro.md b/versioned_docs/version-0.12.0/reference/api/python/micro.md new file mode 100644 index 00000000..69db2fda --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/micro.md @@ -0,0 +1,7 @@ +--- +title: tvm.micro +--- + +::: +tvm.micro +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/ndarray.md b/versioned_docs/version-0.12.0/reference/api/python/ndarray.md new file mode 100644 index 00000000..f144d9b5 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/ndarray.md @@ -0,0 +1,19 @@ +--- +title: tvm.runtime.ndarray +--- + +::: automodule +tvm.runtime.ndarray +::: + +::: +tvm.nd.NDArray +::: + +::: autofunction +tvm.nd.array +::: + +::: autofunction +tvm.nd.empty +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/relay/analysis.md b/versioned_docs/version-0.12.0/reference/api/python/relay/analysis.md new file mode 100644 index 00000000..0a82d753 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/relay/analysis.md @@ -0,0 +1,7 @@ +--- +title: tvm.relay.analysis +--- + +::: +tvm.relay.analysis +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/relay/backend.md b/versioned_docs/version-0.12.0/reference/api/python/relay/backend.md new file mode 100644 index 00000000..2d50a660 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/relay/backend.md @@ -0,0 +1,23 @@ +--- +title: tvm.relay.backend +--- + +::: automodule +tvm.relay.backend +::: + +::: +tvm.relay.backend.interpreter +::: + +::: +tvm.relay.backend.te_compiler +::: + +::: +tvm.relay.backend.graph_executor_codegen +::: + +::: +tvm.relay.backend.vm +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/relay/dataflow_pattern.md b/versioned_docs/version-0.12.0/reference/api/python/relay/dataflow_pattern.md new file mode 100644 index 00000000..9c44104a --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/relay/dataflow_pattern.md @@ -0,0 +1,7 @@ +--- +title: tvm.relay.dataflow_pattern +--- + +::: +tvm.relay.dataflow_pattern +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/relay/frontend.md b/versioned_docs/version-0.12.0/reference/api/python/relay/frontend.md new file mode 100644 index 00000000..ca06b419 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/relay/frontend.md @@ -0,0 +1,7 @@ +--- +title: tvm.relay.frontend +--- + +::: +tvm.relay.frontend +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/relay/image.md b/versioned_docs/version-0.12.0/reference/api/python/relay/image.md new file mode 100644 index 00000000..9c214971 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/relay/image.md @@ -0,0 +1,7 @@ +--- +title: tvm.relay.image +--- + +::: +tvm.relay.image +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/relay/index.md b/versioned_docs/version-0.12.0/reference/api/python/relay/index.md new file mode 100644 index 00000000..ee6cb3e8 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/relay/index.md @@ -0,0 +1,7 @@ +--- +title: tvm.relay +--- + +::: +tvm.relay +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/relay/nn.md b/versioned_docs/version-0.12.0/reference/api/python/relay/nn.md new file mode 100644 index 00000000..0b3873f1 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/relay/nn.md @@ -0,0 +1,7 @@ +--- +title: tvm.relay.nn +--- + +::: +tvm.relay.nn +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/relay/testing.md b/versioned_docs/version-0.12.0/reference/api/python/relay/testing.md new file mode 100644 index 00000000..d861cfe1 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/relay/testing.md @@ -0,0 +1,43 @@ +--- +title: tvm.relay.testing +--- + +::: +tvm.relay.testing +::: + +::: +tvm.relay.testing.mlp +::: + +::: +tvm.relay.testing.resnet +::: + +::: +tvm.relay.testing.dcgan +::: + +::: +tvm.relay.testing.mobilenet +::: + +::: +tvm.relay.testing.lstm +::: + +::: +tvm.relay.testing.inception_v3 +::: + +::: +tvm.relay.testing.squeezenet +::: + +::: +tvm.relay.testing.vgg +::: + +::: +tvm.relay.testing.densenet +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/relay/transform.md b/versioned_docs/version-0.12.0/reference/api/python/relay/transform.md new file mode 100644 index 00000000..6578b10b --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/relay/transform.md @@ -0,0 +1,7 @@ +--- +title: tvm.relay.transform +--- + +::: +tvm.relay.transform +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/relay/vision.md b/versioned_docs/version-0.12.0/reference/api/python/relay/vision.md new file mode 100644 index 00000000..9b8332e7 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/relay/vision.md @@ -0,0 +1,7 @@ +--- +title: tvm.relay.vision +--- + +::: +tvm.relay.vision +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/rpc.md b/versioned_docs/version-0.12.0/reference/api/python/rpc.md new file mode 100644 index 00000000..9689387d --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/rpc.md @@ -0,0 +1,7 @@ +--- +title: tvm.rpc +--- + +::: +tvm.rpc +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/runtime.md b/versioned_docs/version-0.12.0/reference/api/python/runtime.md new file mode 100644 index 00000000..cdaa27f3 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/runtime.md @@ -0,0 +1,7 @@ +--- +title: tvm.runtime +--- + +::: +tvm.runtime +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/target.md b/versioned_docs/version-0.12.0/reference/api/python/target.md new file mode 100644 index 00000000..e9ef930e --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/target.md @@ -0,0 +1,7 @@ +--- +title: tvm.target +--- + +::: +tvm.target +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/te.md b/versioned_docs/version-0.12.0/reference/api/python/te.md new file mode 100644 index 00000000..628f883f --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/te.md @@ -0,0 +1,11 @@ +## tvm.te + +::: +tvm.te +::: + +## tvm.te.hybrid + +::: +tvm.te.hybrid +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/tir.md b/versioned_docs/version-0.12.0/reference/api/python/tir.md new file mode 100644 index 00000000..3f82297e --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/tir.md @@ -0,0 +1,23 @@ +## tvm.tir + +::: +tvm.tir +::: + +## tvm.tir.transform + +::: +tvm.tir.transform +::: + +## tvm.tir.analysis + +::: +tvm.tir.analysis +::: + +## tvm.tir.stmt_functor + +::: +tvm.tir.stmt_functor +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/topi.md b/versioned_docs/version-0.12.0/reference/api/python/topi.md new file mode 100644 index 00000000..ae391b50 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/topi.md @@ -0,0 +1,25 @@ +--- +title: tvm.topi +--- + +::: +tvm.topi +::: + +## tvm.topi.nn + +::: +tvm.topi.nn +::: + +## tvm.topi.image + +::: +tvm.topi.image +::: + +## tvm.topi.sparse + +::: +tvm.topi.sparse +::: diff --git a/versioned_docs/version-0.12.0/reference/api/python/vta/index.md b/versioned_docs/version-0.12.0/reference/api/python/vta/index.md new file mode 100644 index 00000000..680bc5f3 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/api/python/vta/index.md @@ -0,0 +1,46 @@ +--- +title: vta +--- + +This document contains the python API to VTA compiler toolchain. + +::: automodule +vta +::: + +## Hardware Information + +::: autofunction +vta.Environment +::: + +::: autofunction +vta.get_env +::: + +## RPC Utilities + +::: autofunction +vta.reconfig_runtime +::: + +::: autofunction +vta.program_fpga +::: + +## Compiler API + +We program VTA using TVM, so the compiler API in vta package is only a +thin wrapper to provide VTA specific extensions. + +::: autofunction +vta.build_config +::: + +::: autofunction +vta.build +::: + +::: autofunction +vta.lower +::: diff --git a/versioned_docs/version-0.12.0/reference/langref/hybrid_script.md b/versioned_docs/version-0.12.0/reference/langref/hybrid_script.md new file mode 100644 index 00000000..2ba7bf22 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/langref/hybrid_script.md @@ -0,0 +1,225 @@ +--- +title: Hybrid Frontend Language Reference +--- + +## Overview + +This hybrid frontend allows users to write preliminary versions of some +idioms that yet have been supported by TVM officially. + +## Features + +### Software Emulation + +Both software emulation and compilation are supported. To define a +function, you need to use `tvm.te.hybrid.script` decorator to indicate +this is a hybrid function: + +``` python +@tvm.te.hybrid.script +def outer_product(a, b): + c = output_tensor((100, 99), 'float32') + for i in range(a.shape[0]): + for j in range(b.shape[0]): + c[i, j] = a[i] * b[j] + return c +a = numpy.random.randn(100) +b = numpy.random.randn(99) +c = outer_product(a, b) +``` + +This decorator will import [Keywords](#keywords) required spontaneously +when software emulation. After software emulation is done, the imported +keywords will be cleaned up. Users do not need worry about keyword +conflict and pollution. + +Every element passed for software emulation in the argument list is +either a python variable or `numpy` numeric type. + +### Backend Compilation + +This function is not encouraged to use, users are encouraged to use the +second interface. The current parse interface looks like: + +``` python +a = tvm.te.placeholder((100, ), name='a') +b = tvm.te.placeholder((99, ), name='b') +parser = tvm.hybrid.parse(outer_product, [a, b]) # return the parser of this function +``` + +If we pass these tvm data structures, like `Tensor`, `Var`, `Expr.*Imm`, +or `tvm.container.Array`, to this function, it returns a op node: + +``` python +a = tvm.te.placeholder((100, ), name='a') +b = tvm.te.placeholder((99, ), name='b') +c = outer_product(a, b) # return the output tensor(s) of the operator +``` + +You can use any methods that can be applied on a TVM `OpNode`, like +create_schedule, although so far, the functionality of schedule is as +limited as `ExternOpNode`. At least, it can be built to LLVM module. + +### Tuning + +Follow up the example above, you can use some tvm like interfaces to +tune the code: + +``` python +i, j = c.op.axis +sch = te.create_schedule(op) +jo, ji = sch.split(j, 4) +sch.vectorize(ji) +``` + +For now, you can use loop annotations (`unroll`, `parallel`, +`vectorize`, and `bind`), loop manipulation (`split` and `fuse`), and +`reorder`. + +::: note +::: title +Note +::: + +This is a preliminary function, so users should be in charge of the +correctness of the functionality after tuning. Specifically, users +should be careful when fusing and reorderding imperfect loops. +::: + +### Loops + +In HalideIR, loops have in total 4 types: `serial`, `unrolled`, +`parallel`, and `vectorized`. + +Here we use `range` aka `serial`, `unroll`, `parallel`, and `vectorize`, +these **4** keywords to annotate the corresponding types of for loops. +The the usage is roughly the same as Python standard `range`. + +Besides all the loop types supported in Halide, `const_range` is +supported for some specific conditions. Sometimes, `tvm.container.Array` +is desired to pass as an argument, but in TVM-HalideIR, there is no such +support that converts `tvm.container.Array` to an `Expr`. Thus, a +limited feature is supported. Users can access containers by either +constants or constants loops annotated. + +``` python +@tvm.te.hybrid.script +def foo(a, b): # b is a tvm.container.Array + c = output_tensor(a.shape, a.dtype) + for i in const_range(len(a)): # because you have b access, i should be explicitly annotated as const_range + c[i] = a[i] + b[i] + return c +``` + +### Variables + +All the mutable variables will be lowered to an array with size 1. It +regards the first store of a variable as its declaration. + +::: note +::: title +Note +::: + +Unlike conventional Python, in hybrid script, the declared variable can +only be used in the scope level it is declared. +::: + +::: note +::: title +Note +::: + +Currently, you can ONLY use basic-typed variables, i.e. the type of the +variable should be either `float32`, or `int32`. +::: + +``` python +for i in range(5): + s = 0 # declaration, this s will be a 1-array in lowered IR + for j in range(5): + s += a[i, j] # do something with s + b[i] = s # you can still use s in this level +a[0] = s # you CANNOT use s here, even though it is allowed in conventional Python +``` + +### Attributes + +So far, ONLY tensors\' `shape` and `dtype` attribute are supported! The +`shape` attribute is essentially a tuple, so you MUST access it as an +array. Currently, only constant-indexed access is supported. + +``` python +x = a.shape[2] # OK! +for i in range(3): + for j in a.shape[i]: # BAD! i is not a constant! + # do something +``` + +### Conditional Statement and Expression + +``` python +if condition1 and condition2 and condition3: + # do something +else: + # do something else +# Select +a = b if condition else c +``` + +However, NO `True` and `False` keyword supported yet. + +### Math Intrinsics + +So far, these math intrinsics, `log`, `exp`, `sigmoid`, `tanh`, `power`, +and `popcount`, are supported. No import is required, just as it is +mentioned in [Software Emulation](#software-emulation), just use it! + +### Array Allocation + +**Under construction, this function will be supported later!** + +Use a function call `allocation(shape, type, share/local)` to declare an +array buffer. The basic usage is roughly the same as a normal +`numpy.array`, and you should access high-dim array in `a[i, j, k]` +fashion instead of `a[i][j][k]`, even for `tvm.container.Array` for +compilation. + +### Thread Bind + +You can also do loop-thread bind by writing code like this: + +``` python +for tx in bind("threadIdx.x", 100): + a[tx] = b[tx] +``` + +### Assert Statement + +Assert statement is supported, you can simply use it as it is in +standard Python. + +``` python +assert cond, mesg +``` + +::: note +::: title +Note +::: + +`Assert` is NOT a function call. Users are encouraged to use assert in +the way presented above \-\-- condition followed by message. It fits +both Python AST and HalideIR. +::: + +### Keywords + +- For keywords: `serial`, `range`, `unroll`, `parallel`, `vectorize`, + `bind`, `const_range` +- Math keywords: `log`, `exp`, `sqrt`, `rsqrt`, `sigmoid`, `tanh`, + `power`, `popcount`, `round`, `ceil_div` +- Allocate keywords: `allocate`, `output_tensor` +- Data type keywords: `uint8`, `uint16`, `uint32`, `uint64`, `int8`, + `int16`, `int32`, `int64`, `float16`, `float32`, `float64` +- Others: `max_num_threads` diff --git a/versioned_docs/version-0.12.0/reference/langref/index.md b/versioned_docs/version-0.12.0/reference/langref/index.md new file mode 100644 index 00000000..5cd9a1db --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/langref/index.md @@ -0,0 +1,37 @@ +--- +title: Language Reference +--- + +This document provides references to embedded languages and IRs in the +TVM stack. + +## Introduction to Relay + +Relay is a functional, differentiable programming language designed to +be an expressive intermediate representation for machine learning +systems. Relay supports algebraic data types, closures, control flow, +and recursion, allowing it to directly represent more complex models +than computation graph-based IRs can. Relay also includes a form of +dependent typing using *type relations* in order to handle shape +analysis for operators with complex requirements on argument shapes. + +Relay is extensible by design and makes it easy for machine learning +researchers and practitioners to develop new large-scale program +transformations and optimizations. + +The below pages describe the grammar, type system, algebraic data types, +and operators in Relay, respectively. + +``` +relay_expr relay_type relay_adt relay_op relay_pattern +``` + +## Hybrid Script + +The below page describes the TVM hybrid script front-end, which uses +software emulation to support some constructs not officially supported +in TVM. + +``` +hybrid_script +``` diff --git a/versioned_docs/version-0.12.0/reference/langref/relay_adt.md b/versioned_docs/version-0.12.0/reference/langref/relay_adt.md new file mode 100644 index 00000000..320cee40 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/langref/relay_adt.md @@ -0,0 +1,543 @@ +--- +title: Algebraic Data Types in Relay +--- + +Algebraic data types (ADTs) are a staple feature of functional +programming languages, particularly those derived from ML, because they +express data structures in a manner that is easy to reason about when +writing recursive computations. Because recursion is intended to be one +of the primary mechanisms of control flow in Relay, it is important that +Relay include ADTs in order to best express loops and other control flow +structures that must be implemented using recursion. + +## Defining and Matching on an ADT + +*Note: ADTs are not presently supported in the text format. The syntax +here is speculative, based on ADTs in other languages.* + +ADTs can be understood as a generalized version of `enum` and `struct` +types from C-like languages. Like a C `struct:`, an ADT instance is a +container for fields of specified types, but the type system allows for +the same type to encode different possible groupings of fields in a +systematic manner, similar to C `enum` types, which are defined using a +finite set of possible values named by the user. + +Specifically, an ADT is defined as a named group of constructors, each +of which is a function that takes values of specified types as arguments +and returns an instance of the named ADT. An ADT instance simply +contains the values of the arguments passed to the constructor call used +to produce it. + +An ADT value is opaque until it is *deconstructed*, allowing the +arguments to the constructor to be accessed again and used to compute +new values. Because a particular ADT can have multiple constructors with +different signatures, it is usually necessary to branch on the different +possible constructors, resulting in the *match* syntax for ADTs. Hence, +ADTs are sometimes called \"tagged unions\" because an ADT instance is +tagged by the name of the constructor used to produce it and can later +be inspected based on the tag. + +Because each ADT has a finite set of constructors, it is straightforward +to determine whether a function processing an ADT instance is handling +all possible cases. In particular, the type system can ensure that types +are properly assigned in all cases when deconstructing an ADT instance, +in contrast to `union` types in C. Hence, it is often easy to reason +about ADTs. + +*Implementation detail: Relay ADT definitions are global and are stored +in the module, similarly to global function definitions. An ADT name is, +in fact, a global type variable (just as a global function name is a +global variable). The module keeps a mapping of ADT names (global type +variables) to the list of constructors for that ADT.* + +Below is a simple example of defining an ADT and using it in a function +via a match expression: + +``` +# Defines an ADT named "Numbers" +data Numbers { + Empty : () -> Numbers + Single : (Tensor[(), int32]) -> Numbers + Pair : (Tensor[(), int32], Tensor[(), int32]) -> Numbers +} +# A Numbers value can be produced using an Empty, Single, or Pair +# constructor, each with a signature given above + +def @sum(%n : Numbers[]) -> Tensor[(), int32] { + # The match expression branches on the constructor that was + # used to produce %n. The variables in each case are bound + # if the constructor matches that used for %n + match(%n) { + case Empty() { 0 } + case Single(x) { x } + case Pair(x, y) { x + y } + } +} + +@sum(Empty()) # evaluates to 0 +@sum(Single(3)) # evaluates to 3 +@sum(Pair(5, 6)) # evaluates to 11 +``` + +Note that ADTs are identified by name, meaning that two ADTs with +structurally identical constructors will nevertheless be distinct data +types from the point of view of the typechecker. + +``` +# structurally identical constructors to Numbers +data Numbers2 { + Empty2 : () -> Numbers2 + Single2 : (Tensor[(), int32]) -> Numbers2 + Pair2 : (Tensor[(), int32], Tensor[(), int32]) -> Numbers2 +} + +# the below results in a type error because Numbers2 +# is a distinct type from Numbers +# fn() { @sum(Empty2()) } +``` + +## Type-Checking ADTs and Polymorphism + +This section will go into more specific detail about the typing of ADTs. +Most of the complexity involved results from the fact that, as with +functions, ADTs can be polymorphic and take type parameters. + +For example, one of the standard ADTs commonly used in functional +programming languages is the optional type, defined here: + +``` +# a is a type parameter +data Optional { + None : () -> Optional + Some : (a) -> Optional +} +``` + +Optional types are commonly used as the return type for any operation +involving querying into a data structure (returning `Some(v)` if a value +is found and `None` if it isn\'t). Taking a type parameter in the +definition allows the same optional type to be used in a wide variety of +situations, rather than having to define a unique ADT for each different +type that could be contained in it. + +However, it is important to ensure that option types whose contents are +of different types can still be distinguished by the type system, since +it would violate type safety if a function expecting an option +containing a `Tensor[(), int32]` instead receives an option containing a +`Tensor[(3, 4), float32]`. As this example may imply, an ADT instance is +thus given a type that contains the concrete type arguments for that +instance, ensuring the information is kept around. Let the below example +illustrate: + +``` +# the signature for option indicates the type argument +def @inc_scalar(%opt : Optional[Tensor[(), int32]]) -> Tensor[(), int32] { + match(%opt) { + case None() { 1 } + case Some(%s) { %s + 1 } + } +} + +def @main() { + let %one : Optional[Tensor[(), int32]] = Some(1); + let %big : Optional[Tensor[(10, 10), float32]] + = Some(Constant(1, (10, 10), float32)); + let %two = inc_scalar(%one); + # let %bigger = inc_scalar(%big); # type system rejects + # None does not take an argument so it can always implicitly + # be given the correct type arguments + let %z = inc_scalar(None()); + () +} +``` + +The syntax for the annotated type arguments (e.g., +`Optional[Tensor[(), int32]]`) in the above examples is called a \"type +call,\" treating the polymorphic ADT definition as a type-level function +(taking type params and returning a type, namely the ADT). Any ADT +appearing in a type annotation or function signature must be annotated +with type arguments (a non-polymorphic ADT must be in a type call with +no arguments). + +Thus, we can say in general that if constructor `C` that takes arguments +of types `T1, ..., Tn` is a constructor for an ADT `D` that takes type +parameters `v1, ..., vn` (where `T1, ..., Tn` may contain any of the +`v1, ..., vn`), then `C` has the type +`fun(T1, ..., Tn) -> D[v1, ..., vn]`. This means that +constructors are typed like ordinary functions and thus appear inside +call nodes and can be passed to or returned by other functions. In +particular, the `Some` example above has the signature +`fun(a) -> Optional[a]`, while `None` has the signature +`fun() -> Optional[a]`. + +## Recursion with ADTs + +ADT definitions are allowed to be recursive, that is, a definition for +an ADT named `D` can assume the existence of type `D` and use it as an +argument to constructors. Recursion allows ADTs to represent complex +structures such as lists or trees; it is the source of much of ADTs\' +power in functional programming, since an appropriately designed data +structure could make it easy to concisely express a computation with a +recursive function. + +Many commonly used ADTs involve recursion; some of these are given in +[Common ADT Uses](#common-adt-uses). As an example here, we will examine +the list ADT, ubiquitous in functional languages: + +``` +data List { + Nil : () -> List + Cons : (a, List[a]) -> List +} +``` + +(Notice that the recursive reference to `List` is wrapped in a type call +even in the constructor.) + +The above definition means that a list of values of a particular type +can be represented by nesting `Cons` constructors until the end of the +list is reached, which can be indicated with a `Nil` (representing an +empty list). + +Lists represented in this manner can easily be recursively processed. +For example, the following function sums a list of integers: + +``` +def @list_sum(%l : List[Tensor[(), int32]]) -> Tensor[(), int32] { + match(%l) { + case Nil() { 0 } + # add the head of the list to the sum of the tail + case Cons(%h, %t) { %h + @list_sum(%t) } + } +} +``` + +As it happens, many recursive functions on lists like the one just given +share structures that can be factored out into generic, easily usable +functions that will be discussed under [Common ADT +Uses](#common-adt-uses). + +## Pattern Matching in Match Expressions + +Match expressions in Relay, as in other functional languages, are +capable of more versatile pattern matching than simply having one case +for each constructor for the datatype of the value being deconstructed. + +In particular, the patterns in match cases can be built up recursively: + +- Constructor patterns match for a particular ADT constructor. If a + value matches the constructor, each argument to the constructor will + be matched against a nested pattern. +- Wildcard patterns will match any value and will not bind to a + variable. +- Variable patterns will match any value and bind it to a local + variable, scoped to the match clause. + +In the simple case of `@list_sum` above, the first match case has a +`Nil` constructor pattern (with no nested arguments) and the second has +a `Cons` constructor pattern that uses variable patterns for each of the +arguments to `Cons`. + +The below example uses a wildcard pattern to ignore one of the arguments +to `Cons`: + +``` +def @first(%l : List[a]) -> Optional[a] { + match(%l) { + case Nil() { None() } + case Cons(%h, _) { Some(%h) } # list tail is unused and ignored + } +} +``` + +Here, a constructor pattern is nested inside another constructor pattern +to avoid nested match expressions for a list option. A top-level +wildcard pattern is also used to handle all cases that do not match the +first clause: + +``` +def @second_opt(%ll : Optional[List[a]]) -> Optional[a] { + match(%ll) { + # we only need the second member of the list if there is one + case Some(Cons(_, Cons(%s, _))) { Some(%s) } + case _ { None() } + } +} + +# @second_opt(Some(Cons(1, Nil()))) evaluates to None() +# @second_opt(Some(Cons(1, Cons(2, Nil())))) evaluates to Some(2) +# @second_opt(Some(Nil())) evaluates to None() +# @second_opt(None()) evaluates to None() +``` + +Note that a match expression checks its patterns in the order the cases +are listed: the first clause whose pattern that matches the input value +is the one that is evaluated. Here, a top-level variable pattern binds +the whole input value: + +``` +def @match_order_beware(%l : List[a]) -> List[a] { + match(%l) { + case %v { %v } + # the above matches everything so neither of these runs + case Cons(%h, %t) { Cons(%h, @match_order_beware(%t)) } + case Nil() { Nil() } + } +} +``` + +## Common ADT Uses + +In functional programming languages, certain ADTs provide useful +facilities for writing common programs. Parametric polymorphism and +higher-order functions allow these ADTs to be easily reuseable and for +generic functions to manipulate them in common situations. Relay +includes a \"Prelude\" of certain pre-defined ADTs and functions for +them that correspond to the indispensable ADTs of other languages. + +The option type defined under [Type-Checking ADTs and +Polymorphism](#type-checking-adts-and-polymorphism) is one such ADT, +used whenever it can make sense for a function to only return a value +under certain circumstances. Having the option type allows for the type +system to keep track of which functions always return a value of a +certain type versus returning an option of that type, ensuring that any +options are always explicitly checked (contrast with returning null +pointers or throwing exceptions as other ways to addressing that +problem). + +Lists (defined in [Recursion with ADTs](#recursion-with-adts)) can be +manipulated by generic functions in a manner similar to list +comprehensions and certain library functions in Python. Below are very +common functions for iterating through lists, which are included in +Relay\'s Prelude. (These have all been extensively characterized in the +functional programming literature, and we do not attempt to reproduce +that work in this document.) + +``` +# Map: for [h1, h2, ..., hn] returns [f(h1), f(h2), ..., f(hn)] +def @map(%f : fn(a) -> b, %l : List[a]) -> List[b] { + match(%l) { + case Nil() { Nil() } + case Cons(%h, %t) { Cons(%f(%h), @map(%f, %t)) } + } +} + +# Left fold: for [h1, h2, ..., hn] returns f(...(f(f(z, h1), h2)...), hn) +def @foldl(%f : fn(b, a) -> b, %z : b, %l : List[a]) -> b { + match(%l) { + case Nil() { %z } + case Cons(%h, %t) { @foldl(%f, %f(%z, %h), %t) } + } +} + +# Right fold: for [h1, h2, ..., hn] returns f(h1, f(h2, f(..., (f(hn, z)...) +def @foldr(%f : fn(a, b) -> b, %z : b, %l : List[a] -> b { + match(%l) { + case Nil() { %z } + case Cons(%h, %t) { %f(%h, @foldr(%f, %z, %t)) } + } +} +``` + +Using these iteration constructs, many common operations over lists can +be expressed compactly. For example, the following map doubles all +members of a list: + +``` +# directly written +def @double(%l : List[Tensor[(), int32]]) -> List[Tensor[(), int32]] { + match(%l) { + case Nil() { Nil() } + case Cons(%h, %t) { Cons(%h * 2, @double(%t)) } + } +} + +# map takes care of the recursion +@map(fn(%i) { %i * 2 }, %l) +``` + +The following right fold concatenates two lists: + +``` +# directly written +def @concat(%l1 : List[a], %l2 : List[a]) -> List[a] { + match(%l1) { + case Nil() { %l2 } + case Cons(%h, %t) { Cons(%h, @concat(%t, %l2) } + } +} + +# foldr takes care of the recursion +@foldr(fn(%h, %z) { Cons(%h, %z) }, %l2, %l1) +``` + +The following left fold flattens a list of lists (using concatenation): + +``` +# directly written +def @flatten(%ll : List[List[a]]) -> List[a] { + match(%ll) { + case Cons(%h, %t) { @concat(%h, @flatten(%t)) } + case Nil() { Nil() } + } + +# foldl takes care of the recursion +@foldl(@concat, Nil(), %ll) +``` + +Note that these iteration constructs can be implemented directly in +Relay\'s source language and more can easily be defined (and for more +data types, like trees), rather than being constructs built into the +language (e.g., [\"foreach\" in +MXNet](https://mxnet.apache.org/versions/master/tutorials/control_flow/ControlFlowTutorial.html)). +ADTs and their extensibility allow for a broad range of iterations and +data structures to be expressed in Relay and supported by the type +system without having to modify the language implementation. + +## Implementing Neural Nets Using ADTs + +In [this 2015 blog +post](http://colah.github.io/posts/2015-09-NN-Types-FP/), Christopher +Olah notes that many neural networks can be easily expressed using +common functional programming constructs. Relay\'s ADTs allow those +examples to be implemented directly in TVM. + +First let us suppose that we have a function corresponding to a trained +recurrent neural net (RNN) cell, which takes in a past state and an +input value and returns a new state and output value. In Relay, this +would have the following signature: + +``` +@cell : fn(state_type, in_type) -> (state_type, out_type) +``` + +We might consider a ReLU cell as a simple concrete example, with a +trained version below: + +``` +def @linear(%x, %w, %b) { %w*%x + %b } + +def @relu_cell(%w, # weights + %b, # offsets + %s, # state + %x # input +) { + let %x2 = @linear(%x, %w.0, %b.0); + let %s2 = @linear(%s, %w.1, %b.1); + # doesn't change the state + (%s, nn.relu(%x2 + %s2)) +} + +# this is a higher-order function because it returns a closure +def @trained_cell(%w, %b) { + fn(%x, %h) { @relu_cell(%w, %b, %x, %h) } +} +``` + +Following Olah\'s example, we can encode a sequence (list) of inputs +with the following left fold: + +``` +def @encode(%cell, %input : List[in_type], %init : state_type) -> state_type { + # not using the output + @foldl(fn(%state, %in) { %cell(%state, %in).0 }, %init, %input) +} +``` + +Using an *unfold* iterator (from Haskell\'s standard library), the same +cell could be used to make a generator network (which takes a single +input and produces a sequence of outputs): + +``` +# included in Relay's Prelude +def @unfoldr(%f : fn(b) -> Optional[(a, b)], %z : b) -> List[a] { + match(%f(%z)) { + case Some(%pair) { Cons(%pair.0, @unfoldr(%f, %pair.1)) } + case None() { Nil() } + } +} + +# we need some way of generating an input to the cell function given only a state +def @gen_func(%state : state_type) : Optional[(out_type, state_type)] { + let %in : Optional[in_type] = @generate_input(%state); + match(%in) { + case Some(%n) { + let %cell_out = @cell(%n, %state); + Some((%cell_out.1, %cell_out.0)) # pair of output and state + } + case None() { None() } + } +} + +def @generator(%cell, %init : state_type) -> List[out_type] { + @unfoldr(fn(%state) { @gen_func(%cell, %state) }, %init) +} +``` + +An accumulating map (a fold that simultaneously updates an accumulator +value and a list of outputs) can be used to write a general RNN (with an +output for every input): + +``` +def @map_accumr(%f : fn(a, b) -> (a, c), %acc : a, %l : List[b]) -> (a, List[c]) { + match(%l) { + case Nil() { (%acc, Nil()) } + case Cons(%b, %t) { + let %update = %f(%acc, %b); + let %rest = @map_accumr(%f, %update.0, %t)); + (%rest.0, Cons(%update.1, %rest.1)) + } + } +} + +# can also be implemented as a right fold +# (this version is included in Relay's Prelude) +def @map_accumr_fold(%f, %acc, %l) { + @foldr(fn(%b, %p) { + let %f_out = %f(%p.0, %b); + (%f_out.0, Cons(%f_out.1, %p.1)) + }, + (%acc, Nil()), %l) +} + +def @general_rnn(%cell, %init : state_type, %input : List[in_type]) + -> (state_type, List[out_type]) { + @map_accumr(%cell, %init, %input) +} +``` + +Olah also gives an example of a bidirectional neural network, in which +two sets of cells (which may have different weights) process the input +in both directions and produce a single set of outputs. The following is +a Relay implementation of that example: + +``` +# creates a list of tuples from two lists +# included in Relay's Prelude +def @zip(%l : List[a], %m : List[b]) -> List[(a, b)] { + match(%l) { + case Nil() { Nil() } + case Cons(%a, %t1) { + match(%m) { + case Nil() { Nil() } + case Cons(%b, %t2) { Cons((%a, %b), @zip(%t1, %t2)) } + } + } + } +} + +# analogous to map_accumr +# included in Relay's Prelude +def @map_accmul(%f, %acc, %l) { + @foldl(fn(%p, %b){ + let %f_out = %f(%p.0, %b); + (%f_out.0, Cons(%f_out.1, %p.1)) + }, (%acc, Nil()), %l) +} + +def @bidirectional_rnn + (%cell1, %cell2, %state1 : state1_type, %state2 : state2_type, %input : List[in_type]) + -> List[(out1_type, out2_type)] { + @zip(@map_accumr(%cell1, %state1, %input).1, @map_accuml(%cell2, %state2, %input).1) +} +``` diff --git a/versioned_docs/version-0.12.0/reference/langref/relay_expr.md b/versioned_docs/version-0.12.0/reference/langref/relay_expr.md new file mode 100644 index 00000000..42706abe --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/langref/relay_expr.md @@ -0,0 +1,684 @@ +--- +title: Expressions in Relay +--- + +The Relay IR is a pure, expression-oriented language. The below sections +describe the different expressions in Relay and give details of their +semantics. + +## Dataflow and Control Fragments + +For the purposes of comparing Relay to traditional computational +graph-based IRs, it can be useful to consider Relay expressions in terms +of dataflow and control fragments. Each portion of a Relay program +containing expressions that only affect the dataflow can be viewed as a +traditional computation graph when writing and expressing +transformations. + +The dataflow fragment covers the set of Relay expressions that do not +involve control flow. That is, any portion of a program containing only +the following constructs corresponds to a pure computation graph: + +- [Variables](#variables) +- Tuple [Construction](#construction) and [Projection](#projection) +- [Let Bindings](#let-bindings) +- [Graph Bindings](#graph-bindings) +- Calls to [Operators](#operators) and [ADT + Constructors](#adt-constructors) + +Control flow expressions allow the graph topology to change based on the +value of previously executed expressions. The control fragment in Relay +includes the following constructs: + +- [If-Then-Else](#if-then-else) Expressions +- [ADT Matching](#adt-matching) Expressions +- Recursive Calls in Functions + +From the point of view of a computation graph, a function is a subgraph +and a function call inlines the subgraph, substituting its arguments for +the free variables in the subgraph with corresponding names. Thus, if a +function\'s body uses only dataflow constructs, a call to that function +is in the dataflow fragment; conversely, if the function\'s body +contains control flow, a call to that function is not part of the +dataflow fragment. + +## Variables + +Inspired by LLVM, Relay explicitly distinguishes between local and +global variables both in the AST and in the text format. In the text +format, global and local variables are distinguished by prefixes, or +*sigils*. Global variables are prefixed with `@` and local variables +with `%`. + +This explicit distinction makes certain optimizations easier to +implement. For example, inlining a global definition requires no +analysis: simply substituting the definition suffices. + +### Global Variable + +Global identifiers are prefixed by the `@` sigil, such as \"`@global`\". +A global identifier always references a globally visible definition +contained in the globally visible environment, known as the +[module](#module-and-global-functions). Global identifiers must be +unique. + +See `GlobalVar` for +its implementation and documentation. + +### Local Variable + +Local identifiers are prefixed by the `%` sigil, such as \"`%local`\". A +local identifier always references a function argument or a variable +bound in a `let` expression, and will be scoped to the function where it +appears or the `let` expression where it is bound, respectively. + +In the below code segment, notice that `%a` is defined twice. This is +permitted, as in most functional languages; in the scope of the second +`let` expression, the name `%a` is \"shadowed,\" meaning all references +to `%a` in the inner scope refer to the later definition, while +references to `%a` in the outer scope continue to refer to the first +one. + +``` +let %a = 1; +let %b = 2 * %a; // %b = 2 +let %a = %a + %a; // %a = 2. %a is shadowed +%a + %b // has value 2 + 2 = 4 +``` + +(Note that in Relay\'s implementation, each definition of a local +variable creates a new `Var`, so a shadowed local variable, despite having the same +name as one in an outer scope, will be a different object. This allows +for comparing local variables by pointer identity with the knowledge +that the same local variable object corresponds to a different binding +site.) + +See `Var` for its +implementation and documentation. + +## Functions + +Functions in Relay act similarly to procedures or functions in other +programming languages and serve to generalize the concept of a named +subgraph. + +Functions are first class in Relay, which means they are expressions +just like variables, constants, and tuples. Additionally, functions in +Relay are higher-order, which means that a function can be passed as an +argument to a function or returned by a function, as function +expressions evaluate to closures (see the [Closures](#closures) +subsection), which are values like tensors and tuples. + +See `Function` +for the definition and documentation of function nodes. + +### Syntax + +A definition minimally consists of the keyword `fn`, an empty set of +parameters, and a body expression +(`Expr`) contained by +curly braces. + +``` +fn() { body } +``` + +A definition may contain any number of parameters. For example, a simple +function that invokes the `add` operator: + +``` +fn(%x, %y) { add(%x, %y) } +``` + +Notice that within the function\'s body, the parameters are local +variables, just like those bound in a `let` expression. + +One may also annotate explicit types on functions. For example, we can +restrict the above function to only work on certain types: + +``` +fn(%x : Tensor[(10, 10), float32], %y : Tensor[(10, 10), float32]) + -> Tensor[(10, 10), float32] { + add(%x, %y) +} +``` + +The above function only takes arguments of type +`Tensor[(10, 10), float32]` and returns a value of type +`Tensor[(10, 10), float32]`. A function parameter is just a local +variable (`LocalVar`) +optionally annotated with a type, written as `%x : T`. + +When the type information is omitted, Relay attempts to infer the most +general type for the users. This property is known as generalization: +for a definition without explicit annotations, Relay attempts to assign +the most general type to the parameters and return type based on the +function body and call sites. + +A recursive function expression can be defined using a `let` binding, as +here: + +``` +let %fact = fn(%x : Tensor[(10, 10), float32]) -> Tensor[(10, 10), float32] { + if (%x == Constant(0, (10, 10), float32)) { + Constant(1, (10, 10), float32) + } else { + %x * %fact(%x - Constant(1, (10, 10), float32)) + } +}; +%fact(Constant(10, (10, 10), float32)) +``` + +### Closures + +A function expression evaluates to a closure. Closures are values that +are represented as a pair of a local environment (storing the values for +all variables defined outside the scope of the function\'s body) and the +function itself. + +For example, in the below example, the final result will be a tensor of +zero values because the closure for `%f` stores the value of `%x` at the +pointer where `%f` was defined. + +``` +let %g = fn() { + let %x = Constant(0, (10, 10), float32); + // %x is a free variable in the below function + fn(%y) { %y * %x } +}; +// the %x in %g's body is not in scope anymore +// %f is a closure where %x maps to Constant(0, (10, 10), float32) +let %f = %g(); +let %x = Constant(1, (10, 10), float32); +%f(%x) // evaluates to Constant(0, (10, 10), float32) +``` + +### Polymorphism and Type Relations + +*Note: type parameter syntax is not yet supported in the text format.* + +A function may also be given a set of type parameters, which can be +substituted for specific types at call sites. Functions with type +parameters are *type polymorphic*; their return type or the types of +arguments they will accept can vary based on the type arguments given at +call sites. + +Type parameters are classified by *kind* and can only appear in parts of +the type signature where their kind is appropriate (e.g., type +parameters of kind `Shape` can only appear where a shape would be +expected in a tensor type); for a full discussion, see +`the documentation on type parameters `. + +For example, one can define a polymorphic identity function for any +Relay type as follows: + +``` +fn(%x : t) -> t { + %x +} +``` + +The below definition is also polymorphic, but restricts its arguments to +tensor types: + +``` +fn(%x : Tensor[s, bt]) { + %x +} +``` + +Notice that the return type is omitted and will be inferred. + +*Note: \"where\" syntax is not yet supported in the text format.* + +A function may also be subject to one or more type relations, such as in +the following: + +``` +fn(%x, %y) where Broadcast { add(%x, %y) } +``` + +In the above definition, the types of `%x` and `%y` and the return type +are subject to the `Broadcast` relation, meaning all three must be +tensors and their shapes follow the elementwise broadcast relation. As +with operators, the definitions of relations are not transparent to +Relay and they are instead implemented externally in either C++ or +Python. + +As in the case of `Broadcast`, relations are used to express complicated +constraints on types (especially tensor shapes). All function relations +must hold at all call sites; type checking is thus treated as a +constraint-solving problem. For more detail on type relations and their +implementations, please see +`their section in the documentation on Relay's type system `. + +## Operators + +An operator is a primitive operation, such as `add` or `conv2d`, not +defined in the Relay language. Operators are declared in the global +operator registry in C++. Many common operators are backed by TVM\'s +Tensor Operator Inventory. + +To register an operator a user must provide an implementation of the +operator, its type, and any other desired metadata. The operator +registry is a column-based store where operators are keys, so any +metadata (which might be referenced by optimization passes) may be +registered as a new column. + +From the perspective of Relay\'s type system, an operator is a function, +so operators may be called like any other function and have function +types. In particular, operator types are registered using a single type +relation (see +`the documentation on type relations `, typically a relation specialized to that operator. For +example, the `add` operator is registered with the `Broadcast` relation, +indicating that the arguments of `add` must be tensors and that the +return type is a tensor whose shape depends on those of its arguments. + +Operators are rendered without a sigil (e.g `conv2d`, `flatten`) when +pretty-printing Relay programs. Operators are explicitly contained in +the program and are uniquely identifiable by pointer. + +Note that common arithmetic operators such as `add` and `multiply` may +be written using the corresponding arithmetic operators in the text +format (e.g., `+` or `*`) as syntactic sugar. + +See `Op` for the +definition and documentation of operator nodes, demonstrating the +infrastructure for registering operator metadata. The other files in +`op` give handles for +generating a call to various pre-registered operators. The +`tutorial on adding operators to Relay ` shows how to add further operators into the language. + +## ADT Constructors + +Algebraic data types (ADTs) in Relay are described in detail in a +`separate overview` and +their integration into the type system is described +`here`. + +In this section, we will simply note that ADT constructors are given a +function type and should be used inside call nodes like a function or +operator. An ADT constructor is defined by giving the name of the ADT it +constructs (a global type variable) and the types of the expected +arguments for the constructor. + +If the ADT definition includes type variables, those type variables may +appear in the constructor. Constructors cannot include any other type +variables. + +Let us suppose that `D` is an ADT that takes type parameters `a` and +`b`. If `C1` is a constructor for `D` and expects two arguments, one of +type `a` and one of type `b`, then `C1` has the following type +signature: `fun(a, b) -> D[a, b]`. (See either the ADT overview or +the discussion of ADT typing for an explanation of the type call in the +return type.) If another constructor for `D`, `C2`, takes no arguments, +then it has the following type signature: `fun() -> D[a, b]`; the +type parameters will always appear in the return type. + +Once called, a constructor produces an ADT instance, which is a +container that stores the values of the arguments to the constructor as +well as the name (\"tag\") of the constructor. The tag will be used for +deconstructing the instances and retrieving the values when [ADT +Matching](#adt-matching). + +See `Constructor` for +the definition and documentation. + +## Call + +Expressions with function types in Relay are \"callable,\" meaning that +they can be invoked via a function call. These consist of any expression +that evaluates to a closure (i.e., function expressions or global +functions) and Relay operators. + +The syntax of calls follows that used in C-like languages, demonstrated +in the example below: + +``` +let %c = 1; +let %f = fn(%x : Tensor[(), float32], %y : Tensor[(), float32]) { %x + %y + %c }; +%f(10, 11) +``` + +When a closure is called (see [Closures](#closures)), the closure\'s +body is evaluated in the stored environment (i.e., using the stored +values for free variables) with local variable bindings added for each +argument; the final value obtained by evaluating the body is the call\'s +return value. Thus, in the above example, the call evaluates to 22. In +the case of operators, the implementation is opaque to Relay, so the +result is left up to the registered TVM implementation. + +*Note: type parameters are not yet supported in the text format.* + +A type-polymorphic function can also include type arguments at a call +site. The type arguments are substituted for type parameters when type +checking. If a function is type-polymorphic and type arguments are not +given, type inference will attempt to infer type arguments if possible. +The following code gives examples of explicit and inferred type +arguments: + +``` +// %f : fn(a, b) -> c +let %x1 = %f(True, False); +// %x1 is of type Tensor[(), bool] +let %x2 : () = %f(%x1, %x1) +// the type arguments in the second call are inferred to be +``` + +Note that all type relations in the function type must hold at each call +site. Specifically, this means that the relation will be checked against +the specific types of the arguments at a given call site. This is also a +form of polymorphism, since there may be multiple valid assignments of +argument types and a return type so long as the relation is satisfied. + +For example, if we have a function `%f` that takes tensor arguments and +has the `Broadcast` relation, then there are many different shapes that +the arguments in the below call could have that would satisfy the type +annotation: + +``` +let %x : Tensor[(100, 100, 100), float32] = %f(%a, %b); +%x +``` + +See `Call` for its +definition and documentation. + +## Module and Global Functions + +Relay keeps a global data structure known as a \"module\" (often called +an \"environment\" in other functional programming languages) to keep +track of the definitions of global functions. In particular, the module +keeps a globally accessible mapping of global variables to the function +expressions they denote. The utility of the module is that it allows +global functions to recursively refer to themselves or any other global +function (e.g., as in mutual recursion). + +Note Relay\'s module is analogous to data structures for keeping track +of subgraphs in computation graph-based IRs. + +Global functions in Relay behave identically to the function expressions +defined in [Functions](#functions), but have syntactic sugar in the text +format to enter their definitions into the module. Namely, a global +function definition includes a global identifier and is allowed to +recursively refer to that identifier in the body, as in the following +example: + +``` +def @ackermann(%m : Tensor[(), int32], %n : Tensor[(), int32]) -> Tensor[(), int32] { + if (%m == 0) { + %n + 1 + } else if (%m > 0 && %n == 0) { + @ackermann(%m - 1, 1) + } else { + @ackermann(%m - 1, @ackermann(%m, %n - 1)) + } +} +``` + +This definition would result in a module entry mapping the identifier +`@ackermann` to a function expression with the parameters, return type, +and body above. Any reference to the identifier `@ackermann` elsewhere +in the code could then look up the identifier in the module and replace +the function definition as needed. + +See `IRModule` for the +definition and documentation of a module. + +## Constant + +This node represents a constant tensor value (see +`Value` for more details). A +constant is represented as a `NDArray`, allowing Relay to utilize TVM operators for constant +evaluation. + +This node can also represent scalar constants, since scalars are tensors +with a shape of `()`. In the text format, numerical and boolean literals +are thus syntactic sugar for constants encoding a tensor type with a +rank-zero shape. + +See `Constant` for +its definition and documentation. + +## Tuples + +### Construction + +The tuple node builds a finite (that is, of statically known size) +sequence of heterogeneous data. These tuples match Python\'s closely, +and their fixed length allows for efficient projection of their members. + +``` +fn(%a : Tensor[(10, 10), float32], %b : float32, %c : Tensor[(100, 100), float32]) { + let %tup = (%a, %b); // type: (Tensor[(10, 10), float32], float32) + ((%tup.0 + %tup.1), %c) // type: (Tensor[(10, 10), float32], Tensor[(100, 100), float32]) +} +``` + +See `Tuple` for its +definition and documentation. + +### Projection + +A tuple must be indexed by an integer constant in order to extract a +particular member of the tuple. Projections are 0-indexed. + +For example, the below projection evaluates to `%b`: + +``` +(%a, %b, %c).1 +``` + +See `TupleGetItem` +for its definition and documentation. + +## Let Bindings + +A `let` binding is an immutable local variable binding, allowing the +user to bind an expression to a name. + +A `let` binding contains a local variable, an optional type annotation, +a value, and a body expression that may reference the bound identifier. +If a type annotation on the bound variable is omitted, Relay attempts to +infer the most general type permitted for the variable. + +The bound variable in a `let` expression is only in scope in its body, +except when the variable defines a function expression. When a `let` +expression creates a function, the variable is also in scope in its +value to allow for recursively defined functions (see the previous +subsection). + +The value of a `let` binding is the value of the final expression after +evaluating the bindings it depends on. For example, in the following +example the entire expression evaluates to a tensor of shape `(10, 10)` +where all elements are 2: + +``` +let %x : Tensor[(10, 10), float32] = Constant(1, (10, 10), float32); +%x + %x +``` + +A sequence of `let` bindings can be considered as a dataflow graph, +where the bindings are a series of sub-graphs connected by bound +variables. Since these binding sequences are pure, a pair of bindings +where neither depends on the other can be safely reordered. For example, +the first and second `let` bindings below may be evaluated in either +order because neither has a dataflow dependency on the other: + +``` +let %x = %a + %b; +let %y = %c + %d; +%x * %y +``` + +See `Let` for its +definition and documentation. + +## Graph Bindings + +A `let` binding creates a named variable that is bound to the given +value and scoped to the subsequent expression. By contrast, a graph +binding allows for explicitly constructing dataflow graphs in a Relay +program by binding an expression (graph node) directly to a temporary +variable, which is not scoped. Each reference to the variable +corresponds to an edge in the dataflow graph. This has the semantics of +substituting the expression wherever the variable appears, even though +the graph node will only be evaluated once by the compiled program. + +These bindings allow for a style of programming that corresponds to that +already employed by NNVM and other dataflow graph-based input formats. +The fact that the variables are not scoped offers some flexibility in +evaluation order compared to `let` bindings, though this can also +introduce some ambiguity in programs (the +`developer introduction to the Relay IR` includes more detailed discussion of this nuance). + +*Note: Graph bindings are not currently parsed by the text format.* + +In Relay\'s text format, a graph binding can be written as below (note +the lack of a `let` keyword and a semicolon): + +``` +%1 = %a + %b +%2 = %1 + %1 +%2 * %2 +``` + +Unlike a let binding, a graph binding is not represented as an AST node +in Relay, but rather as a meta-variable referencing its AST node value. +For example, a program like the above could be constructed in Relay\'s +Python front-end by setting *Python variables* equal to the +corresponding Relay AST node and using the variables repeatedly, as +below (a C++ program using the corresponding API bindings could +accomplish the same thing): + +``` +sum1 = relay.add(a, b) +sum2 = relay.add(sum1, sum1) +relay.multiply(sum2, sum2) +``` + +For development purposes and to enable certain optimizations, Relay +includes passes to convert between dataflow graphs defined using graph +bindings and programs with `let` bindings in A-normal form, employed by +many compiler optimizations from the functional programming community +(see [\"A-Normalization: Why and How\" by Matt +Might](http://matt.might.net/articles/a-normalization/) for an +introduction to A-normal form). + +## If-Then-Else + +Relay has a simple if-then-else expression that allows programs to +branch on a single value of type `bool`, i.e., a zero-rank tensor of +booleans (`Tensor[(), bool]`). + +``` +if (%t == %u) { + %t +} else { + %u +} +``` + +Since if-then-else branches are expressions, they may appear inline +wherever any other expression may be expected, like invocations of the +ternary operator in C-like languages. The if-then-else expression +evaluates to the value of the \"then\" branch if the condition value +evaluates to `True` and evaluates to the value of the \"else\" branch if +the condition value evaluates to `False`. + +See `If` for its +definition and documentation. + +## ADT Matching + +Instances of algebraic data types (ADTs), as discussed in the +`ADT overview`, are +containers that store the arguments passed to the constructor used to +create them, tagged by the constructor name. + +Match expressions in Relay allow for retrieving the values stored in an +ADT instance (\"deconstructing\" it) based on their constructor tag. A +match expression behaves similarly to a C-style `switch` statement, +branching on the different possible constructors for the type of the +value being deconstructed. As the ADT overview details, match +expressions are capable of more general pattern-matching than simply +splitting by constructors: any ADT instance nested inside an instance +(e.g., a list of lists) can be deconstructed at the same time as the +outer instance, while the different fields of the instance can be bound +to variables. (See `this section` for a detailed description of ADT pattern-matching.) + +A match expression is defined using the input value (an expression) and +a list of clauses, each of which consists of a pattern and an +expression. When executed, the *first* clause whose pattern matches the +structure of the queried value is executed; the clause expression is +evaluated and returned. + +For example, suppose we have an ADT for natural numbers: + +``` +data Nat { + Z : () -> Nat # zero + S : (Nat) -> Nat # successor (+1) to a nat +} +``` + +Then the following function subtracts one from a passed nat: + +``` +fn(%v: Nat[]) -> Nat[] { + match(%v) { + case Z() { Z() } + case S(%n) { %n } # the variable %n is bound in the scope of this clause + } +} +``` + +The following function subtracts two from its argument if it is at least +two and returns the argument otherwise, using a nested constructor +pattern: + +``` +fn(%v : Nat[]) -> Nat[] { + match(%v) { + case S(S(%n)) { %n } + # wildcard pattern: matches all cases not matched already + case _ { %v } + } +} +``` + +As aforementioned, the ordering of match clauses is relevant. In the +below example, the first clause will always match so those below it can +never run: + +``` +fn(%v : Nat[]) -> Nat[] { + match(%v) { + case _ { %v } + case S(S(%n)) { S(%n) } + case S(%n) { %n } + case Z() { S(Z()) } + } +} +``` + +See `Match` for its +definition and documentation. + +## TempExprs + +Program transformations (passes) in Relay may require inserting +temporary state into the program AST to guide further transformations. +The `TempExpr` node is provided as a utility to developers for this +purpose; nodes inheriting from `TempExpr` cannot appear directly in +user-provided code but may be inserted in a pass. Any `TempExpr` created +in a pass should ideally be eliminated before the pass is complete, as a +`TempExpr` only stores internal state and has no semantics of its own. + +For an example of `TempExpr` being used in a pass, see +`src/relay/transforms/fold_scale_axis.cc`, which uses `TempExpr` nodes +to store information about scaling parameters as the pass tries to fold +these into the weights of a convolution. + +See `TempExpr` for +its definition and documentation. diff --git a/versioned_docs/version-0.12.0/reference/langref/relay_op.md b/versioned_docs/version-0.12.0/reference/langref/relay_op.md new file mode 100644 index 00000000..bb237532 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/langref/relay_op.md @@ -0,0 +1,131 @@ +--- +title: Relay Core Tensor Operators +--- + +This page contains the list of core tensor operator primitives +pre-defined in tvm.relay. The core tensor operator primitives cover +typical workloads in deep learning. They can represent workloads in +front-end frameworks and provide basic building blocks for optimization. +Since deep learning is a fast evolving field, it is possible to have +operators that are not in here. + +::: note +::: title +Note +::: + +This document will directly list the function signature of these +operators in the python frontend. +::: + +## Overview of Operators + +**Level 1: Basic Operators** + +This level enables fully connected multi-layer perceptron. + +::: +tvm.relay.log tvm.relay.sqrt tvm.relay.rsqrt tvm.relay.exp +tvm.relay.sigmoid tvm.relay.add tvm.relay.subtract tvm.relay.multiply +tvm.relay.divide tvm.relay.mod tvm.relay.tanh tvm.relay.concatenate +tvm.relay.expand_dims tvm.relay.nn.softmax tvm.relay.nn.log_softmax +tvm.relay.nn.relu tvm.relay.nn.dropout tvm.relay.nn.batch_norm +tvm.relay.nn.bias_add +::: + +**Level 2: Convolutions** + +This level enables typical convnet models. + +::: +tvm.relay.nn.conv2d tvm.relay.nn.conv2d_transpose tvm.relay.nn.conv3d +tvm.relay.nn.conv3d_transpose tvm.relay.nn.dense tvm.relay.nn.max_pool2d +tvm.relay.nn.max_pool3d tvm.relay.nn.avg_pool2d tvm.relay.nn.avg_pool3d +tvm.relay.nn.global_max_pool2d tvm.relay.nn.global_avg_pool2d +tvm.relay.nn.upsampling tvm.relay.nn.upsampling3d +tvm.relay.nn.batch_flatten tvm.relay.nn.pad tvm.relay.nn.lrn +tvm.relay.nn.l2_normalize tvm.relay.nn.bitpack +tvm.relay.nn.bitserial_dense tvm.relay.nn.bitserial_conv2d +tvm.relay.nn.contrib_conv2d_winograd_without_weight_transform +tvm.relay.nn.contrib_conv2d_winograd_weight_transform +tvm.relay.nn.contrib_conv3d_winograd_without_weight_transform +tvm.relay.nn.contrib_conv3d_winograd_weight_transform +::: + +**Level 3: Additional Math And Transform Operators** + +This level enables additional math and transform operators. + +::: +tvm.relay.nn.leaky_relu tvm.relay.nn.prelu tvm.relay.reshape +tvm.relay.reshape_like tvm.relay.copy tvm.relay.transpose +tvm.relay.squeeze tvm.relay.floor tvm.relay.ceil tvm.relay.sign +tvm.relay.trunc tvm.relay.clip tvm.relay.round tvm.relay.abs +tvm.relay.negative tvm.relay.take tvm.relay.zeros tvm.relay.zeros_like +tvm.relay.ones tvm.relay.ones_like tvm.relay.gather tvm.relay.gather_nd +tvm.relay.full tvm.relay.full_like tvm.relay.cast tvm.relay.reinterpret +tvm.relay.split tvm.relay.arange tvm.relay.meshgrid tvm.relay.stack +tvm.relay.repeat tvm.relay.tile tvm.relay.reverse +tvm.relay.reverse_sequence tvm.relay.unravel_index +tvm.relay.sparse_to_dense +::: + +**Level 4: Broadcast and Reductions** + +::: +tvm.relay.right_shift tvm.relay.left_shift tvm.relay.equal +tvm.relay.not_equal tvm.relay.greater tvm.relay.greater_equal +tvm.relay.less tvm.relay.less_equal tvm.relay.all tvm.relay.any +tvm.relay.logical_and tvm.relay.logical_or tvm.relay.logical_not +tvm.relay.logical_xor tvm.relay.maximum tvm.relay.minimum +tvm.relay.power tvm.relay.where tvm.relay.argmax tvm.relay.argmin +tvm.relay.sum tvm.relay.max tvm.relay.min tvm.relay.mean +tvm.relay.variance tvm.relay.std tvm.relay.mean_variance +tvm.relay.mean_std tvm.relay.prod tvm.relay.strided_slice +tvm.relay.broadcast_to +::: + +**Level 5: Vision/Image Operators** + +::: +tvm.relay.image.resize1d tvm.relay.image.resize2d +tvm.relay.image.resize3d tvm.relay.image.crop_and_resize +tvm.relay.image.dilation2d tvm.relay.vision.multibox_prior +tvm.relay.vision.multibox_transform_loc tvm.relay.vision.nms +tvm.relay.vision.yolo_reorg +::: + +**Level 6: Algorithm Operators** + +::: +tvm.relay.argsort tvm.relay.topk +::: + +**Level 10: Temporary Operators** + +This level support backpropagation of broadcast operators. It is +temporary. + +::: +tvm.relay.broadcast_to_like tvm.relay.collapse_sum_like +tvm.relay.slice_like tvm.relay.shape_of tvm.relay.ndarray_size +tvm.relay.layout_transform tvm.relay.device_copy +tvm.relay.annotation.on_device tvm.relay.reverse_reshape +tvm.relay.sequence_mask tvm.relay.nn.batch_matmul +tvm.relay.nn.adaptive_max_pool2d tvm.relay.nn.adaptive_avg_pool2d +tvm.relay.one_hot +::: + +**Level 11: Dialect Operators** + +This level supports dialect operators. + +::: +tvm.relay.qnn.op.add tvm.relay.qnn.op.batch_matmul +tvm.relay.qnn.op.concatenate tvm.relay.qnn.op.conv2d +tvm.relay.qnn.op.conv2d_transpose tvm.relay.qnn.op.dense +tvm.relay.qnn.op.dequantize tvm.relay.qnn.op.mul +tvm.relay.qnn.op.quantize tvm.relay.qnn.op.requantize +tvm.relay.qnn.op.rsqrt tvm.relay.qnn.op.simulated_dequantize +tvm.relay.qnn.op.simulated_quantize tvm.relay.qnn.op.subtract +::: diff --git a/versioned_docs/version-0.12.0/reference/langref/relay_pattern.md b/versioned_docs/version-0.12.0/reference/langref/relay_pattern.md new file mode 100644 index 00000000..ef92242f --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/langref/relay_pattern.md @@ -0,0 +1,572 @@ +--- +title: Pattern Matching in Relay +--- + +There are many places in TVM where we identify pure data-flow sub-graphs +of the Relay program and attempt to transform them in some way example +passes include fusion, quantization, external code generation, and +device specific optimizations such as bitpacking, and layer slicing used +by VTA. + +Many of these passes today require a lots of boring boilerplate code in +order to implement as well as requiring users to think in terms of +visitors and AST matching. Many of these transformations can easily be +described in terms of graph rewrites. In order to build a rewriter or +other advanced machinery we first need a language of patterns to +describe what we can match. + +Such a language is not just useful for building a rewriter but also +providing extension points for existing passes. For example the fusion +pass could be parameterized by a set of fusion patterns which describes +the capability of your hardware, and the quantization pass could take a +set of patterns which describe which operators can be quantized on a +given platform. + +In the backend world, we could use the same machinery to build a higher +level API using bring your own code generation. This API takes set of +patterns describing your hardware capabilities and an external compiler, +providing a relatively smooth heterogeneous experience out of the box. + +## Pattern Examples + +There are quite a few properties of operators that are worth matching. +Below we examine how to match tree properties, and expand on some use +cases that are not fully explored in the prototype. This section +demonstrates how to write patterns. It is recommended to check +[tests/python/relay/test_dataflow_pattern.py](https://github.com/apache/tvm/blob/main/tests/python/relay/test_dataflow_pattern.py) +for more use cases. + +::: note +::: title +Note +::: + +If you cannot find the corresponding pattern node to match the Relay +node you want, you are welcome to raise an issue or submit a PR to add +it. +::: + +### Matching One of Two Ops + +The first example is a simple case where we want to match one operator +with a single input OR another operator with a single input: + +``` python +def test_match_op_or(): + is_add_or_sub = is_op('add') | is_op('subtract') + assert is_add_or_sub.match(relay.op.op.get("add")) + assert is_add_or_sub.match(relay.op.op.get("subtract")) +``` + +### Matching an Op with Attributes + +The next example is a dense operation with any operator that is marked +element-wise: + +``` python +def test_no_match_attr(): + op = is_op('nn.dense').has_attr({"TOpPattern": K_ELEMWISE}) + op_pat = op(wildcard(), wildcard()) + x = relay.var('x') + y = relay.var('y') + assert not op_pat.match(relay.op.nn.dense(x, y)) +``` + +Here is another example to match an op with a specific attribute: + +``` python +def test_match_data_layout(): + is_conv2d = is_op('nn.conv2d')(wildcard(), wildcard()).has_attr({"data_layout": "NHWC"}) + x = relay.var('x') + y = relay.var('y') + assert not is_conv2d.match(relay.op.nn.conv2d(x, y)) +``` + +Or a convolution with a specific kernel size: + +``` python +def test_match_kernel_size(): + is_conv2d = is_op("nn.conv2d")(wildcard(), wildcard()).has_attr({"kernel_size": [3, 3]}) + x = relay.var('x') + y = relay.var('y') + assert is_conv2d.match(relay.op.nn.conv2d(x, y, kernel_size=[3, 3])) +``` + +### Matching an Optional Op + +The next example is matching a pattern with one optional operator. In +this pattern, we can match the graph of conv2d+bias_add+relu or the +graph of conv2d+bias_add. + +``` python +def test_match_optional(): + conv_node = is_op('nn.conv2d')(wildcard(), wildcard()) + bias_node = is_op('nn.bias_add')(conv_node, wildcard()) + pat = bias_node.optional(lambda x: is_op('nn.relu')(x)) + + x = relay.var('x') + y = relay.var('y') + z = relay.var('z') + conv2d = relay.op.nn.conv2d(x, y) + bias = relay.op.nn.bias_add(conv2d, z) + assert pat.match(bias) + relu = relay.op.nn.relu(bias) + assert pat.match(relu) +``` + +### Matching Types + +In addition to matching ops with attributes, we can also make a pattern +to match their types, in interms of the shape and data type. Here are +some examples: + +``` python +def test_match_type(): + # Match any op with float32 + pat1 = has_dtype('float32') + x = relay.var('x', shape=(10, 10), dtype='float32') + assert pat1.match(x) + + # Match any op with shape (10, 10) + pat2 = has_shape((10, 10)) + x = relay.var('x', shape=(10, 10), dtype='float32') + assert pat2.match(x) + + # Match conv2d+relu with a certain shape + conv2d = is_op('nn.conv2d')(wildcard(), wildcard()) + pat3 = is_op('nn.relu')(conv2d).has_shape((1, 32, 28, 28)) + + x = relay.var('x', shape=(1, 3, 28, 28), dtype='float32') + w = relay.var('w', shape=(32, 3, 3, 3), dtype='float32') + conv2d = relay.nn.conv2d(x, w, strides=(1, 1), padding=(1, 1)) + relu = relay.nn.relu(conv2d) + assert pat3.match(relu) +``` + +### Matching Non-Call Nodes + +Sometimes we may also want to match a pattern that includes Tuple or +TupleGetItem nodes. Since there are not call nodes, we need to use +specific pattern nodes to match them: + +``` python +def test_match_tuple(): + x = relay.var('x') + y = relay.var('y') + z = relay.var('z') + tuple_pattern = is_tuple((wildcard(), wildcard(), wildcard())) + assert tuple_pattern.match(relay.expr.Tuple((x,y,z))) +``` + +The next example is matching a pattern of batch_norm -\> get(0) -\> +relu. Note that you can also use +[is_tuple_get_item(bn_node)] to match a +[TupleGetItem] node with any index. + +``` python +def test_match_tuple_get_item(): + bn_node = is_op('nn.batch_norm')(wildcard(), wildcard(), wildcard(), wildcard(), wildcard()) + tuple_get_item_node = is_tuple_get_item(bn_node, 0) + pat = is_op('nn.relu')(tuple_get_item_node) + + x = relay.var('x', shape=(1, 8)) + gamma = relay.var("gamma", shape=(8,)) + beta = relay.var("beta", shape=(8,)) + moving_mean = relay.var("moving_mean", shape=(8,)) + moving_var = relay.var("moving_var", shape=(8,)) + bn_node = relay.nn.batch_norm(x, gamma, beta, moving_mean, moving_var) + tuple_get_item_node = bn_node[0] + out = relay.nn.relu(tuple_get_item_node) + pat.match(out) +``` + +If we have a pattern that crosses a function boundary, we might want to +match the Function itself + +``` python +def test_match_func(): + x = relay.var("x") + y = relay.var("y") + wc1 = wildcard() + wc2 = wildcard() + func_pattern = FunctionPattern([wc1, wc2], wc1 + wc2) + assert func_pattern.match(relay.Function([x, y], x + y)) +``` + +The next example is matching a constant node regarding its values. This +is useful to check if a specific parameter in a subgraph has been bound +or not. + +``` python +def test_match_constant(): + conv2d = is_op('nn.conv2d')(wildcard(), is_constant()) + pattern = is_op('nn.bias_add')(conv2d, wildcard()) + + x = relay.var('x', shape=(1, 3, 224, 224)) + w = relay.var('w', shape=(3, 3, 3, 3)) + b = relay.var('b', shape=(3, )) + conv2d = relay.op.nn.conv2d(x, w) + out = relay.op.nn.bias_add(conv2d, b) + func = relay.Function([x, w, b], out) + mod = tvm.IRModule.from_expr(func) + + # Two inputs of the conv2d in the graph are VarNode by default, so no match. + assert not pattern.match(mod['main'].body) + + # The second input (weight) has been bind with constant values so it is now a constant node. + mod["main"] = bind_params_by_name(mod["main"], + {'w': tvm.nd.array(np.ones(shape=(3, 3, 3, 3)))}) + assert pattern.match(mod['main'].body) +``` + +On the other hand, if you need to match the constant with a specific +value, you can directly use `is_expr`. This could be useful for +algebraic simplify. + +``` python +def test_match_plus_zero(): + zero = (is_expr(relay.const(0)) | is_expr(relay.const(0.0))) + pattern = wildcard() + zero + + x = relay.Var('x') + y = x + relay.const(0) + assert pattern.match(y) +``` + +The next example is matching function nodes with a specific attribute: + +``` python +def test_match_function(): + pattern = wildcard().has_attr({"Composite": "add"}) + + x = relay.var('x') + y = relay.var('y') + f = relay.Function([x, y], x + y).with_attr("Composite", "add") + assert pattern.match(f) +``` + +A Relay `If` expression can be matched if all of its condition, true +branch and false branch are matched: + +``` python +def test_match_if(): + x = is_var("x") + y = is_var("y") + pat = is_if(is_op("less")(x, y), x, y) + + x = relay.var("x") + y = relay.var("y") + cond = x < y + + assert pat.match(relay.expr.If(cond, x, y)) +``` + +A Relay `Let` expression can be matched if all of its variable, value, +and body are matched: + +``` python +def test_match_let(): + x = is_var("x") + y = is_var("y") + let_var = is_var("let") + pat = is_let(let_var, is_op("less")(x, y), let_var) + + x = relay.var("x") + y = relay.var("y") + lv = relay.var("let") + cond = x < y + assert pat.match(relay.expr.Let(lv, cond, lv)) +``` + +### Matching Diamonds and Post-Dominator Graphs + +The next example is matching a diamond with two inputs at the top of the +diamond: + + def test_match_diamond(): + # Pattern + is_conv2d = is_op('nn.conv2d')(is_var(), is_var()) + path1 = is_op('nn.relu')(is_conv2d) + path2 = is_op('nn.leaky_relu')(is_conv2d) + diamond = is_op('add')(path1, path2) + + # Expr + inp = relay.var('input') + weight = relay.var('weight') + conv2d = relay.op.nn.conv2d(inp, weight) + relu = relay.op.nn.relu(conv2d) + leaky_relu = relay.op.nn.leaky_relu(conv2d, alpha=0) + out = relu + leaky_relu + + # Check + assert diamond.match(out) + +The final example is matching diamonds with a post-dominator +relationship. We embed dominator analysis as type of matching in the +pattern language in order to allow for pattern matching with unknown +topology. This is important because we want to be able to use the +language to describe fuse patterns, like elementwise operations followed +by a conv2d: + + def test_match_dom_diamond(): + # Pattern + is_conv2d = is_op('nn.conv2d')(is_var(), is_var()) + reduction = is_op('add')(wildcard(), wildcard()) + diamond = dominates(is_conv2d, is_elemwise, reduction) + + # Expr + inp = relay.var('input') + weight = relay.var('weight') + conv2d = relay.op.nn.conv2d(inp, weight) + relu = relay.op.nn.relu(conv2d) + leaky_relu = relay.op.nn.leaky_relu(conv2d, alpha=0) + out = relu + leaky_relu + + # Check + assert diamond.match(out) + +## Matching Fuzzy Patterns + +The Dominator analysis above lets one match a subgraph of Relay AST that +doesn\'t correspond to a set of patterns nodes exactly 1-to-1. There are +a few other places where we support such \"fuzzy\" matching. + +Tuples, Functions, and Call nodes with any number of inputs can be +matched by passing [None] as the argument value, i.e.: + + tuple_pattern = is_tuple(None) + func_pattern = FunctionPattern(None, wildcard() + wildcard()) + call_pattern = func_pattern(None) + +These patterns allow matching more generic classes patterns by +constraining the use of the arguments rather than the number of +arguments. + +Additionally, we support matching Functions with fuzzy bodies, i.e., a +function body that is under constrained by the pattern. The pattern +[FunctionPattern(\[is_var(), is_var()\], wildcard() + +wildcard()\])] will match [relay.Function(\[x, y\], x + +y)], but it will also match [relay.Function(\[x, y\], x \* +x + y)]. In the second case, the pattern doesn\'t perfectly +constrain the body of the function, so the resulting match is fuzzy. + +## Pattern Language Design + +The pattern language proposed is designed to be a mirror of Relay\'s IR +with additional support for common scenarios. The goal of the pattern +language is to provide a regular-expression like capability for matching +data-flow graphs and doing rewriting. + +The high level design is to introduce a language of patterns for now we +propose the language as: + + Pattern ::= expr + | * + | pattern(pattern1, ... patternN) + | has_type(type) + | has_dtype(type) + | has_shape(shape) + | has_attr(attrs) + | is_var(name) + | is_constant() + | is_expr(expr) + | is_op(op_name) + | is_tuple() + | is_tuple_get_item(pattern, index = None) + | is_if(cond, tru, fls) + | is_let(var, value, body) + | pattern1 `|` pattern2 + | dominates(parent_pattern, path_pattern, child_pattern) + | FunctionPattern(params, body) + +The above language then provides a matching interface with both can +select sub-graphs as well as verify that the graph does match the +pattern. + +### Expression Pattern + +Match a literal expression. + +### Wildcard + +Match any expression. + +### Type Pattern + +Check that the expression matched by the nested pattern has a particular +type. + +### DType Pattern + +Check that the expression matched by the nested pattern has a particular +data type. + +### Shape Pattern + +Check that the expression matched by the nested pattern has a particular +output shape. + +### Attribute Pattern + +Check that the operator matched by the pattern has an attribute with a +particular value. + +### Variable Pattern + +Check that the expression is a relay Variable, and optional provide a +name to match to the Variable name. + +### Alternate + +Either match the first pattern or the second pattern. + +### Domination + +Match child pattern, find a match for the parent pattern, insuring that +the child ultimately dominates the parent (i.e., no nodes outside the +pattern use outputs of the parent), and that ever node between the child +and the pattern matches the path pattern. + +### Function Pattern + +Match a Function with a body and parameters + +### If Pattern + +Match an If with condition, true branch, and false branch + +### Let Pattern + +Match a Let with a variable, value, and body + +## Applications + +The pattern language provides not only the pattern matching but also +pattern processing. Here we introduce two pattern processing approaches +and provide some examples. + +### Pattern Rewriting + +If you would like to replace the matched pattern with another subgraph, +you can leverage the `rewrite` transformation. Here is an example of +rewriting a series of arithmetic operators with a single batch_norm op. +The constructor parameter `require_type` indicates whether InferType is +required to be run before the callback. + +``` python +class BatchnormCallback(DFPatternCallback): + # A callback class to rewrite the matched pattern to a batch_norm op. + def __init__(self, require_type=False): + super().__init__(require_type) + self.x = wildcard() + self.var = wildcard() + self.mean = wildcard() + self.beta = wildcard() + self.gamma = wildcard() + self.eps = wildcard() + + self.pattern = self.gamma * (self.x - self.mean)/is_op("sqrt")(self.var + self.eps) + self.beta + + def callback(self, pre, post, node_map): + x = node_map[self.x][0] + var = node_map[self.var][0] + mean = node_map[self.mean][0] + beta = node_map[self.beta][0] + gamma = node_map[self.gamma][0] + eps = node_map[self.eps][0] + return relay.op.nn.batch_norm(x, gamma, beta, mean, var, epsilon = eps.data.numpy().item())[0] + + # A graph of arithmetic operators that are functional equivalent to batch_norm. + x = relay.var('x') + var = relay.var('var') + mean = relay.var('mean') + beta = relay.var('beta') + gamma = relay.var('gamma') + BN = gamma * (x - mean)/relay.op.sqrt(var + relay.const(1e-5)) + beta + + from tvm.relay.dataflow_pattern import rewrite + out = rewrite(BatchnormCallback(), BN) + assert tvm.ir.structural_equal(out, relay.op.nn.batch_norm(x, gamma, beta, mean, var, epsilon = 1e-5)[0]) +``` + +The function `def callback(self, pre, post, node_map)` will be invoked +when the rewriter matches `self.pattern`. `node_map` is a dictionary +mapping from pattern nodes to matched nodes in the graph. + +The callback function will be invoked recursively on the returned +pattern until the pattern stops changing. As a result, if `self.pattern` +matches any part of the graph that the callback returned, the rewriter +will run in a loop. If you want to avoid multiple rewrites, you can pass +a `rewrite_once=True` parameter to the constructor. + +### Pattern Partitioning + +If you would like to perform a more complex processing for matched +subgraphs and you are not satisfied with `rewrite`, you may consider +partitioning the matched subgraphs to a separate Relay function and +perform other processes to the function. Here we use `pattern.partition` +to create a new Relay function for each matched subgraph. The +functionality is similar to the op fusion pass in TVM: + +``` python +# A pattern matching conv2d+relu. +pattern = is_op("nn.relu")(is_op("nn.conv2d")(wildcard(), wildcard())) + +# A graph. +x = relay.var('input') +w = relay.var('weight') +conv2d = relay.op.nn.conv2d(x, w) +relu = relay.op.nn.relu(conv2d) +print('relu') +# free_var %x: Tensor[(1, 3, 224, 224), float32] +# free_var %w: Tensor[(3, 3, 3, 3), float32] +# %0 = nn.conv2d(%x, %w, padding=[0, 0, 0, 0]) /* ty=Tensor[(1, 3, 222, 222), float32] */; +# free_var %b: Tensor[(3), float32] +# nn.bias_add(%0, %b) /* ty=Tensor[(1, 3, 222, 222), float32] */ + +# After partition. +print(pattern.partition(relu)) +# free_var %x: Tensor[(1, 3, 224, 224), float32] +# free_var %w: Tensor[(3, 3, 3, 3), float32] +# free_var %b: Tensor[(3), float32] +# %1 = fn (%FunctionVar_0_0, %FunctionVar_0_1, +# %FunctionVar_0_2, PartitionedFromPattern="nn.conv2d_nn.bias_add_") { +# %0 = nn.conv2d(%FunctionVar_0_0, %FunctionVar_0_1, padding=[0, 0, 0, 0]); +# nn.bias_add(%0, %FunctionVar_0_2) +# }; +# %1(%x, %w, %b) +``` + +Note that you can also specify the attributes for the created functions: + +``` python +print(pattern.partition(relu, {'Composite': 'one_layer'})) +# free_var %x: Tensor[(1, 3, 224, 224), float32] +# free_var %w: Tensor[(3, 3, 3, 3), float32] +# free_var %b: Tensor[(3), float32] +# %1 = fn (%FunctionVar_0_0, %FunctionVar_0_1, +# %FunctionVar_0_2, Composite="one_layer", +# PartitionedFromPattern="nn.conv2d_nn.bias_add_") { +# %0 = nn.conv2d(%FunctionVar_0_0, %FunctionVar_0_1, padding=[0, 0, 0, 0]); +# nn.bias_add(%0, %FunctionVar_0_2) +# }; +# %1(%x, %w, %b) +``` + +If you need a customized checking function that cannot be specified +using pattern language, you can specify `check` function when +partitioning. The following example demonstrates a case that checks +input data layout of a subgraph: + +``` python +def check(pre): + conv = pre.args[0] + return (conv.attrs.data_layout == "NCHW") and bool(conv.checked_type.shape[0] == 1) + +pattern.partition(relu, check=check) +``` + +In this example, we check if the first argument of the matched subgraph +(i.e., `pre.args[0]`) has data layout \"NCHW\" and if its batch size +is 1. This feature is useful if the conditions of matching a pattern +cannot be verified by analyzing the pattern itself. diff --git a/versioned_docs/version-0.12.0/reference/langref/relay_type.md b/versioned_docs/version-0.12.0/reference/langref/relay_type.md new file mode 100644 index 00000000..4e469ee1 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/langref/relay_type.md @@ -0,0 +1,385 @@ +--- +title: Relay\'s Type System +--- + +We briefly introduced types while detailing Relay\'s expression +language, but have not yet described its type system. Relay is a +statically typed and type-inferred language, allowing programs to be +fully typed while requiring just a few explicit type annotations. + +Static types are useful when performing compiler optimizations because +they communicate properties about the data a program manipulates, such +as runtime shape, data layout, and storage, without needing to run the +program. Relay\'s [Algebraic Data Types](#algebraic-data-types) allow +for easily and flexibly composing types in order to build data +structures that can be reasoned about inductively and used to write +recursive functions. + +Relay\'s type system features a form of *dependent typing* for shapes. +That is, its type system keeps track of the shapes of tensors in a Relay +program. Treating tensor shapes as types allows Relay to perform more +powerful reasoning at compile time; in particular, Relay can statically +reason about operations whose output shapes vary based on the input +shapes in complex ways. Casting shape inference as a type inference +problem allows Relay to infer the shapes of all tensors at compile time, +including in programs that use branching and function calls. + +Statically reasoning about shapes in this manner allows Relay to be +ahead-of-time compiled and provides much more information about tensors +for optimizations further in the compilation pipeline. Such +optimizations can be implemented as passes, which are Relay-to-Relay AST +transformations, and may use the inferred types (e.g., shape +information) for making decisions about program transformations. For +instance, `src/relay/transforms/fuse_ops.cc` gives an implementation of +a pass that uses inferred tensor shapes to replace invocations of +operators in a Relay program with fused operator implementations. + +Reasoning about tensor types in Relay is encoded using *type relations*, +which means that the bulk of type checking in Relay is constraint +solving (ensuring that all type relations are satisfied at call sites). +Type relations offer a flexible and relatively simple way of making the +power of dependent typing available in Relay without greatly increasing +the complexity of its type system. + +Below we detail the language of types in Relay and how they are assigned +to Relay expressions. + +## Type + +The base type for all Relay types. All Relay types are sub-classes of +this base type. + +See `Type` for its +definition and documentation. + +## Tensor Type + +A concrete tensor type in Relay. + +Tensors are typed according to data type and shape. At present, these +use TVM\'s data types and shapes, but in the future, Relay may include a +separate AST for shapes. In particular, data types include `bool`, +`float32`, `int8` and various other bit widths and numbers of lanes. +Shapes are given as tuples of dimensions (TVM `IndexExpr`), such as +`(5, 5)`; scalars are also given tuple types and have a shape of `()`. + +Note, though, that TVM shapes can also include variables and arithmetic +expressions including variables, so Relay\'s constraint solving phase +will attempt to find assignments to all shape variables to ensure all +shapes will be concrete before running a program. + +For example, here is a simple concrete tensor type corresponding to a +10-by-10 tensor of 32-bit floats: + +``` +Tensor[(10, 10), float32] +``` + +See `TensorType` for +its definition and documentation. + +## Tuple Type + +A type of a tuple in Relay. + +Just as a tuple is simply a sequence of values of statically known +length, the type of a tuple consists of a sequence of the types +corresponding to each member of the tuple. + +Because a tuple type is of statically known size, the type of a tuple +projection is simply the corresponding index into the tuple type. + +For example, in the below code, `%t` is of type +`(Tensor[(), bool], Tensor[(10, 10), float32])` and `%c` is of type +`Tensor[(10, 10), float32]`. + +``` +let %t = (False, Constant(1, (10, 10), float32)); +let %c = %t.1; +%c +``` + +See `TupleType` for its +definition and documentation. + +## Type Parameter + +Type parameters represent placeholder types used for polymorphism in +functions. Type parameters are specified according to *kind*, +corresponding to the types those parameters are allowed to replace: + +- `Type`, corresponding to top-level Relay types like tensor types, + tuple types, and function types +- `BaseType`, corresponding to the base type of a tensor (e.g., + `float32`, `bool`) +- `Shape`, corresponding to a tensor shape +- `ShapeVar`, corresponding to variables within a tensor shape + +Relay\'s type system enforces that type parameters are only allowed to +appear where their kind permits them, so if type variable `t` is of kind +`Type`, `Tensor[t, float32]` is not a valid type. + +Like normal parameters, concrete arguments must be given for type +parameters at call sites. + +For example, `s` below is a type parameter of kind `Shape` and it will +be substituted with `(10, 10)` at the call site below: + +``` +def @plus(%t1 : Tensor[s, float32], %t2 : Tensor[s, float32]) { + add(%t1, %t2) +} +plus<(10, 10)>(%a, %b) +``` + +See `TypeVar` for its +definition and documentation. + +## Type Constraint + +This is an abstract class representing a type constraint, to be +elaborated upon in further releases. Currently, type relations are the +only type constraints provided; they are discussed below. + +See `TypeConstraint` +for its definition and documentation. + +## Function Type + +A function type in Relay, see [tvm/relay/type.h] for more +details. + +This is the type assigned to functions in Relay. A function type +consists of a list of type parameters, a set of type constraints, a +sequence of argument types, and a return type. + +We informally write function types as: +`fn(arg_types) -> ret_type where type_constraints` + +A type parameter in the function type may appear in the argument types +or the return types. Additionally, each of the type constraints must +hold at every call site of the function. The type constraints typically +take the function\'s argument types and the function\'s return type as +arguments, but may take a subset instead. + +See `FuncType` for its +definition and documentation. + +## Type Relation + +A type relation is the most complex type system feature in Relay. It +allows users to extend type inference with new rules. We use type +relations to define types for operators that work with tensor shapes in +complex ways, such as broadcasting operators or `flatten`, allowing +Relay to statically reason about the shapes in these cases. + +A type relation `R` describes a relationship between the input and +output types of a Relay function. Namely, `R` is a function on types +that outputs [true] if the relationship holds and +[false] if it fails to hold. Types given to a relation may +be incomplete or include shape variables, so type inference must assign +appropriate values to incomplete types and shape variables for necessary +relations to hold, if such values exist. + +For example we can define an identity relation to be: + +``` prolog +Identity(I, I) :- true +``` + +It is usually convenient to type operators in Relay by defining a +relation specific to that operator that encodes all the necessary +constraints on the argument types and the return type. For example, we +can define the relation for `flatten`: + +``` prolog +Flatten(Tensor(sh, bt), O) :- + O = Tensor(sh[0], prod(sh[1:])) +``` + +If we have a relation like `Broadcast` it becomes possible to type +operators like `add`: + +``` +add : fn(t1, t2) -> t3 + where Broadcast +``` + +The inclusion of `Broadcast` above indicates that the argument types and +the return type must be tensors where the shape of `t3` is the broadcast +of the shapes of `t1` and `t2`. The type system will accept any argument +types and return type so long as they fulfill `Broadcast`. + +Note that the above example relations are written in Prolog-like syntax, +but currently the relations must be implemented by users in C++ or +Python. More specifically, Relay\'s type system uses an *ad hoc* solver +for type relations in which type relations are actually implemented as +C++ or Python functions that check whether the relation holds and +imperatively update any shape variables or incomplete types. In the +current implementation, the functions implementing relations should +return `False` if the relation fails to hold and `True` if the relation +holds or if there is not enough information to determine whether it +holds or not. + +The functions for all the relations are run as needed (if an input is +updated) until one of the following conditions holds: + +1. All relations hold and no incomplete types remain (typechecking + succeeds). +2. A relation fails to hold (a type error). +3. A fixpoint is reached where shape variables or incomplete types + remain (either a type error or more type annotations may be needed). + +Presently all of the relations used in Relay are implemented in C++. See +the files in `src/relay/op` for examples of relations implemented in +C++. + +See `TypeRelation` for +its definition and documentation. + +## Incomplete Type + +An incomplete type is a type or portion of a type that is not yet known. +This is only used during type inference. Any omitted type annotation is +replaced by an incomplete type, which will be replaced by another type +at a later point. + +Incomplete types are known as \"type variables\" or \"type holes\" in +the programming languages literature. We use the name \"incomplete +type\" in order to more clearly distinguish them from type parameters: +Type parameters must be bound to a function and are replaced with +concrete type arguments (instantiated) at call sites, whereas incomplete +types may appear anywhere in the program and are filled in during type +inference. + +See `IncompleteType` +for its definition and documentation. + +## Algebraic Data Types + +*Note: ADTs are not currently supported in the text format.* + +Algebraic data types (ADTs) are described in more detail in +`their overview `; this +section describes their implementation in the type system. + +An ADT is defined by a collection of named constructors, each of which +takes arguments of certain types. An instance of an ADT is a container +that stores the values of the constructor arguments used to produce it +as well as the name of the constructor; the values can be retrieved by +deconstructing the instance by matching based on its constructor. Hence, +ADTs are sometimes called \"tagged unions\": like a C-style union, the +contents of an instance for a given ADT may have different types in +certain cases, but the constructor serves as a tag to indicate how to +interpret the contents. + +From the type system\'s perspective, it is most pertinent that ADTs can +take type parameters (constructor arguments can be type parameters, +though ADT instances with different type parameters must be treated as +different types) and be recursive (a constructor for an ADT can take an +instance of that ADT, thus an ADT like a tree or list can be inductively +built up). The representation of ADTs in the type system must be able to +accommodate these facts, as the below sections will detail. + +### Global Type Variable + +To represent ADTs compactly and easily allow for recursive ADT +definitions, an ADT definition is given a handle in the form of a global +type variable that uniquely identifies it. Each ADT definition is given +a fresh global type variable as a handle, so pointer equality can be +used to distinguish different ADT names. + +For the purposes of Relay\'s type system, ADTs are differentiated by +name; that means that if two ADTs have different handles, they will be +considered different types even if all their constructors are +structurally identical. + +Recursion in an ADT definition thus follows just like recursion for a +global function: the constructor can simply reference the ADT handle +(global type variable) in its definition. + +See `GlobalTypeVar` for +its definition and documentation. + +### Definitions (Type Data) + +Besides a name, an ADT needs to store the constructors that are used to +define it and any type parameters used within them. These are stored in +the module, +`analogous to global function definitions`. + +While type-checking uses of ADTs, the type system sometimes must index +into the module using the ADT name to look up information about +constructors. For example, if a constructor is being pattern-matched in +a match expression clause, the type-checker must check the +constructor\'s signature to ensure that any bound variables are being +assigned the correct types. + +See `TypeData` for its +definition and documentation. + +### Type Call + +Because an ADT definition can take type parameters, Relay\'s type system +considers an ADT definition to be a *type-level function* (in that the +definition takes type parameters and returns the type of an ADT instance +with those type parameters). Thus, any instance of an ADT is typed using +a type call, which explicitly lists the type parameters given to the ADT +definition. + +It is important to list the type parameters for an ADT instance, as two +ADT instances built using different constructors but the same type +parameters are of the *same type* while two ADT instances with different +type parameters should not be considered the same type (e.g., a list of +integers should not have the same type as a list of pairs of floating +point tensors). + +The \"function\" in the type call is the ADT handle and there must be +one argument for each type parameter in the ADT definition. (An ADT +definition with no arguments means that any instance will have no type +arguments passed to the type call). + +See `TypeCall` for its +definition and documentation. + +### Example: List ADT + +This subsection uses the simple list ADT (included as a default ADT in +Relay) to illustrate the constructs described in the previous sections. +Its definition is as follows: + +``` +data List { + Nil : () -> List + Cons : (a, List[a]) -> List +} +``` + +Thus, the global type variable `List` is the handle for the ADT. The +type data for the list ADT in the module notes that `List` takes one +type parameter and has two constructors, `Nil` (with signature +`fn() -> List[a]`) and `Cons` (with signature +`fn(a, List[a]) -> List[a]`). The recursive reference to `List` in +the `Cons` constructor is accomplished by using the global type variable +`List` in the constructor definition. + +Below two instances of lists with their types given, using type calls: + +``` +Cons(1, Cons(2, Nil())) # List[Tensor[(), int32]] +Cons((1, 1), Cons((2, 2), Nil())) # List[(Tensor[(), int32], Tensor[(), int32])] +``` + +Note that `Nil()` can be an instance of any list because it does not +take any arguments that use a type parameter. (Nevertheless, for any +*particular* instance of `Nil()`, the type parameter must be specified.) + +Here are two lists that are rejected by the type system because the type +parameters do not match: + +``` +# attempting to put an integer on a list of int * int tuples +Cons(1, Cons((1, 1), Nil())) +# attempting to put a list of ints on a list of lists of int * int tuples +Cons(Cons(1, Cons(2, Nil())), Cons(Cons((1, 1), Cons((2, 2), Nil())), Nil())) +``` diff --git a/versioned_docs/version-0.12.0/reference/publications.md b/versioned_docs/version-0.12.0/reference/publications.md new file mode 100644 index 00000000..5c1666b3 --- /dev/null +++ b/versioned_docs/version-0.12.0/reference/publications.md @@ -0,0 +1,52 @@ +--- +title: Publications +--- + +TVM is developed as part of peer-reviewed research in machine learning +compiler framework for CPUs, GPUs, and machine learning accelerators. + +This document includes references to publications describing the +research, results, and design that use or built on top of TVM. + +2018 + +- [TVM: An Automated End-to-End Optimizing Compiler for Deep + Learning](https://arxiv.org/abs/1802.04799), + \[[Slides](https://www.usenix.org/system/files/osdi18-chen.pdf)\] +- [Learning to Optimize Tensor + Programs](https://mlsys.org/media/mlsys-2021/Slides/1507.pdf), + \[Slides\] + +2020 + +- [Ansor: Generating High-Performance Tensor Programs for Deep + Learning](https://arxiv.org/abs/2101.08458), + \[[Slides](https://assets.amazon.science/c2/46/2481c9064a8bbaebcf389dd5ad75/lorien-efficient-deep-learning-workloads-delivery.pdf)\] + \[[Tutorial](https://arxiv.org/abs/2105.03215)\] + +2021 + +- [Nimble: Efficiently Compiling Dynamic Neural Networks for Model + Inference](https://tvm.apache.org/2020/07/15/how-to-bring-your-own-codegen-to-tvm), + \[[Slides](https://proceedings.mlsys.org/paper/2022/file/fa7cdfad1a5aaf8370ebeda47a1ff1c3-Paper.pdf)\] +- [Cortex: A Compiler for Recursive Deep Learning + Models](https://proceedings.mlsys.org/paper/2022/file/38b3eff8baf56627478ec76a704e9b52-Paper.pdf), + \[[Slides](https://arxiv.org/abs/2110.10221)\] +- [UNIT: Unifying Tensorized Instruction + Compilation](https://arxiv.org/pdf/1805.08166.pdf), \[Slides\] +- [Lorien: Efficient Deep Learning Workloads + Delivery](https://arxiv.org/abs/2006.06762), \[Slides\] +- [Bring Your Own Codegen to Deep Learning + Compiler](https://www.usenix.org/sites/default/files/conference/protected-files/osdi20_slides_zheng.pdf), + \[Slides\] + \[[Tutorial](https://tvm.apache.org/2021/03/03/intro-auto-scheduler)\] + +2022 + +- [DietCode: Automatic optimization for dynamic tensor + program](https://arxiv.org/abs/2006.03031), \[Slides\] +- [Bolt: Bridging the Gap between Auto-tuners and Hardware-native + Performance](https://shenhaichen.com/slides/nimble_mlsys.pdf), + \[Slides\] +- [The CoRa Tensor Compiler: Compilation for Ragged Tensors with + Minimal Padding](https://arxiv.org/pdf/2011.01383.pdf), \[Slides\] diff --git a/versioned_docs/version-0.12.0/topic/_category_.json b/versioned_docs/version-0.12.0/topic/_category_.json new file mode 100644 index 00000000..c04647be --- /dev/null +++ b/versioned_docs/version-0.12.0/topic/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "主题指南", + "position": 600 +} diff --git a/versioned_docs/version-0.12.0/topic/microtvm/index.md b/versioned_docs/version-0.12.0/topic/microtvm/index.md new file mode 100644 index 00000000..2b91dfd1 --- /dev/null +++ b/versioned_docs/version-0.12.0/topic/microtvm/index.md @@ -0,0 +1,39 @@ +--- +title: microTVM:裸机上的 TVM +--- + +# microTVM:裸机上的 TVM + +microTVM 在裸机(即物联网)设备上运行 TVM 模型。microTVM 只依赖于 C 标准库,不需要操作系统来执行。 microTVM 目前正在全力开发中。 + +![/img/docs/tlc-pack/web-data/main/images/dev/microtvm_workflow.svg](/img/docs/tlc-pack/web-data/main/images/dev/microtvm_workflow.svg) + +microTVM 是: + +* TVM 编译器的扩展,允许 TVM 以微控制器为目标 +* 一种在设备上运行 TVM RPC 服务器的方法,允许自动调优 +* 最小 C runtime,支持裸机设备上的独立模型推理。 + +## 支持的硬件 + +microTVM 目前测试支持 Zephyr RTOS 的 Cortex-M 微控制器;不过,它灵活,且可移植到 RISC-V 等其他处理器,也无需 Zephyr。当前的 demo 针对 QEMU 和以下硬件运行: + +* [STM Nucleo-F746ZG](https://www.st.com/en/evaluation-tools/nucleo-f746zg.html) +* [STM STM32F746 Discovery](https://www.st.com/en/evaluation-tools/32f746gdiscovery.html) +* [nRF 5340 开发套件](https://www.nordicsemi.com/Software-and-tools/Development-Kits/nRF5340-DK) + +## microTVM 入门 + +在使用 microTVM 之前,推荐使用支持的开发板。然后,按照这些教程开始使用 microTVM: + +1. [启动 microTVM 虚拟机参考手册](/docs/how_to/microtvm/microtvm_vm)。microTVM 教程依赖于 Zephyr 和硬件的编译器工具链。通过虚拟机参考手册,可快速安装这些依赖。 +2. 尝试使用 [TFLite 教程的 microTVM](/docs/how_to/microtvm/microtvm_tflite)。 +3. 尝试运行更复杂的 [CIFAR10-CNN 模型](https://github.com/areusch/microtvm-blogpost-eval)。 + +## microTVM 的工作原理 + +可以在 [microTVM 设计文档](/docs/arch/arch/microtvm_design) 中阅读有关这些部件设计的更多信息。 + +## 帮助及讨论 + +推荐访问 [TVM 论坛](https://discuss.tvm.ai/),查看历史帖子并探讨 microTVM 的相关问题。 diff --git a/versioned_docs/version-0.12.0/topic/vta/dev/_category_.json b/versioned_docs/version-0.12.0/topic/vta/dev/_category_.json new file mode 100644 index 00000000..70a2b90c --- /dev/null +++ b/versioned_docs/version-0.12.0/topic/vta/dev/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 200 +} diff --git a/versioned_docs/version-0.12.0/topic/vta/dev/config.md b/versioned_docs/version-0.12.0/topic/vta/dev/config.md new file mode 100644 index 00000000..2e16d21e --- /dev/null +++ b/versioned_docs/version-0.12.0/topic/vta/dev/config.md @@ -0,0 +1,36 @@ +--- +title: VTA 配置 +--- + +# VTA 配置 + +VTA 堆栈包含硬件加速器堆栈和基于 TVM 的软件堆栈。VTA 具有开箱即用的灵活性:通过修改高级配置文件 `3rdparty/vta-hw/config/vta_config.json`,用户可以更改张量内联函数的 shape、时钟频率、流水线、数据类型宽度和芯片缓冲区大小。 + +## 参数概述 + +下表解释了 `vta_config.json` 文件中列出的参数。 + +| **属性** | **格式** | **描述** | +|:---|:---|:---| +| TARGET | String | TVM 设备 target。 | +| HW_VER | String | VTA 硬件版本号。 | +| LOG_INP_WIDTH | Int (log2) | 输入数据类型有符号整数宽度。 | +| LOG_WGT_WIDTH | Int (log2) | 权重数据类型有符号整数宽度。 | +| LOG_ACC_WIDTH | Int (log2) | 累加器数据类型有符号整数宽度。 | +| LOG_BATCH | Int (log2) | VTA 矩阵乘以固有输入/输出维度 0。 | +| LOG_BLOCK | Int (log2) | VTA 矩阵乘以内部维度。 | +| LOG_UOP_BUFF_SIZE | Int (log2) | 以字节为单位的微操作片上缓冲区。 | +| LOG_INP_BUFF_SIZE | Int (log2) | 以字节为单位输入芯片上缓冲区。 | +| LOG_WGT_BUFF_SIZE | Int (log2) | 以字节为单位的芯片上缓冲区权重。 | +| LOG_ACC_BUFF_SIZE | Int (log2) | 以字节为单位的累加器芯片上缓冲区。 | + +:::note +当参数名称以 `LOG` 开头时,表示它描述的值只能表示为 2 的幂。因此,用这些参数的 log2 值来描述它们。例如,为了描述输入数据类型的 8 位整数宽度,将 `LOG_INP_WIDTH` 设置为 3,即 log2(8)。类似地,要描述 64kB 微操作缓冲区,将 `LOG_UOP_BUFF_SIZE` 设置为 16。 +::: + +每个参数的详细信息: + +* `TARGET`:可设置为 `"pynq"`、`"ultra96"`、`"sim"`(快速模拟器)或 `"tsim"`(用验证器来循环精确模拟)。 +* `HW_VER`:硬件版本,每次 VTA 硬件设计更改时都会增加。此参数用于唯一标识硬件比特流。 +* `LOG_BATCH`:等价于 shape (A, B) x (B, C) 的乘积中的 A,或者通常来说是,内部张量计算的 batch 维度。 +* `LOG_BLOCK`:相当于 shape (A, B) x (B, C) 中的 B 和 C,或者通常来说是,内部张量计算的输入/输出通道维度。 \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/topic/vta/dev/hardware.md b/versioned_docs/version-0.12.0/topic/vta/dev/hardware.md new file mode 100644 index 00000000..88b14ae1 --- /dev/null +++ b/versioned_docs/version-0.12.0/topic/vta/dev/hardware.md @@ -0,0 +1,190 @@ +--- +title: VTA 硬件指南 +--- + +# VTA 硬件指南 + +我们提供了 VTA 硬件设计的自上而下的概述。本硬件设计指南包含两个级别的 VTA 硬件: + +* VTA 设计及其 ISA 硬件-软件接口的架构概述。 +* VTA 硬件模块的微架构概述,以及计算 core 的微代码规范。 + +## VTA 概述 + +VTA 是一种通用深度学习加速器,专为快速高效的密集线性代数而构建。VTA 包含一个简单的类似 RISC 的处理器,可以在秩(rank)为 1 或 2 的张量寄存器上执行密集线性代数运算。此外,该设计采用解耦的访问执行,来隐藏内存访问延迟。 + +在更广的范围内,VTA 可以作为全栈优化的模板深度学习加速器设计,将通用张量计算接口提供给编译器栈。 + +![/img/docs/uwsampl/web-data/main/vta/blogpost/vta_overview.png](/img/docs/uwsampl/web-data/main/vta/blogpost/vta_overview.png) + +上图给出了 VTA 硬件组织的高级概述。VTA 由四个模块组成,它们通过 FIFO 队列和本地内存块(SRAM)相互通信,实现任务级 pipeline 并行: + +* fetch 模块负责从 DRAM 加载指令流。它将这些指令解码,并将它们路由到三个命令队列的任意一个。 +* load 模块负责将输入和权重张量从 DRAM 加载到数据专用的芯片存储器中。 +* compute 模块用其 GEMM 内核执行密集线性代数计算,并用其张量 ALU 执行一般计算。它还负责将数据从 DRAM 加载到寄存器文件中,并将微操作内核加载到微操作缓存中。 +* store 模块将计算 core 产生的结果存储回 DRAM。 + +## HLS 硬件源代码组织 + +VTA 设计目前在 Vivado HLS C++ 中指定,只有 Xilinx 工具链支持。 VTA 硬件源代码包含在 `3rdparty/vta-hw/hardware/xilinx/sources` 目录下: + +* `vta.cc` 包含所有 VTA 模块的定义,以及顶层 VTA 设计的顶层行为模型。 +* `vta.h` 包含用 Xilinx `ap_int` 类型实现的类型定义,以及函数原型声明。 + +此外,预处理器宏定义在 `3rdparty/vta-hw/include/vta/hw_spec.h` 目录下。这些宏定义大部分来自 `3rdparty/vta-hw/config/vta_config.json` 文件中列出的参数。 + +json 文件由 `3rdparty/vta-hw/config/vta_config.py` 处理,生成一个编译标志的字符串,来定义预处理器的宏。 + +makefile 文件用这个字符串,在 HLS 硬件综合编译器和构建 VTA runtime 的 C++ 编译器中,设置这些高级参数。 + +### HLS 模块示例 + +以下是一个 VTA 模块在 C++ 中的定义: + +``` c++ +void fetch( + uint32_t insn_count, + volatile insn_T *insns, + hls::stream &load_queue, + hls::stream &gemm_queue, + hls::stream &store_queue) { +#pragma HLS INTERFACE s_axilite port = insn_count bundle = CONTROL_BUS +#pragma HLS INTERFACE m_axi port = insns offset = slave bundle = ins_port +#pragma HLS INTERFACE axis port = load_queue +#pragma HLS INTERFACE axis port = gemm_queue +#pragma HLS INTERFACE axis port = store_queue +#pragma HLS INTERFACE s_axilite port = return bundle = CONTROL_BUS + + INSN_DECODE: for (int pc = 0; pc < insn_count; pc++) { +#pragma HLS PIPELINE II = 1 + // Read instruction fields + // 读取指令字段 + insn_T insn = insns[pc]; + // Do some partial decoding + // 做部分解码 + opcode_T opcode = insn.range(VTA_INSN_MEM_0_1, VTA_INSN_MEM_0_0); + memop_id_T memory_type = insn.range(VTA_INSN_MEM_5_1, VTA_INSN_MEM_5_0); + // Push to appropriate instruction queue + // 推送到合适的指令队列 + if (opcode == VTA_OPCODE_STORE) { + store_queue.write(insn); + } else if (opcode == VTA_OPCODE_LOAD && + (memory_type == VTA_MEM_ID_INP || memory_type == VTA_MEM_ID_WGT)) { + load_queue.write(insn); + } else { + gemm_queue.write(insn); + } + } +} +``` + +**关于 HLS 编码的一些观点:** + +* *参数*:每个函数的参数列表和接口编译指示,定义了生成的硬件模块公开的硬件接口。 + * 按值传递的参数表示的是,主机可以写入的只读硬件内存映射寄存器。例如,这个 fetch 函数有一个 `insn_count` 参数,该参数将被合成为主机写入的内存映射寄存器,设置给定 VTA 指令序列的长度。 + * 根据所用的接口编译指示,指针参数可以是: + * 与 `m_axi` 接口编译指示一起使用时,将生成 AXI 请求者接口,提供对 DRAM 的 DMA 访问。 + * 与 `bram` 接口编译指示一起使用时,生成 BRAM 接口,将读和/或写端口公开到 FPGA block-RAM。 + * 将推理传递的 HLS 流与 `axis` 接口编译指示结合,产生模块的 FIFO 接口。硬件 FIFO 在模块之间提供了一种有用的同步机制。 +* *编译指示**(pragmas)*:要定义每个模块的硬件实现,编译器编译指示是必不可少的。下面列出了 VTA 设计中使用的几个编译指示,其作用是将实现要求传递给编译器。 + * `HLS INTERFACE`:指定合成硬件模块的接口。 + * `HLS PIPELINE`:通过设置启动间隔目标来定义硬件 pipeline 性能 target。当设置 `II == 1` target 时,它告诉编译器合成的硬件 pipeline 能在每个周期执行一次循环迭代。 + * `HLS DEPENDENCE`:指示编译器忽略给定循环中某些类型的依赖检查。一个对相同 BRAM 结构进行写和读的循环体,需要 II 为 1。HLS 编译器必须假设最坏的情况,即:向之前写操作更新循环的地址发出读操作:鉴于 BRAM 时序特性,这是无法实现的(至少需要 2 个周期才能看到更新的值)。因此,为了实现 II 为 1,必须放宽依赖检查。注意,当打开此优化时,它会进入软件堆栈,防止写入后读取相同的地址。 + +:::note +本 [参考指南](https://www.xilinx.com/support/documentation/sw_manuals/xilinx2018_2/ug902-vivado-high-level-synthesis.pdf) 给出了 Xilinx 2018.2 工具链更深入、更完整的 HLS 规范。 +::: + +## 架构概述 + +### 指令集架构 + +VTA 的指令集架构(instruction set architecture,简称 ISA)由 4 条具有可变执行延迟的 CISC 指令组成,其中两条指令通过执行微编码指令序列来执行计算。 + +下面列出了 VTA 指令: + +* `LOAD` 指令:将 2D 张量从 DRAM 加载到输入缓冲区、权重缓冲区或寄存器文件中。它还可以将微内核加载到微操作缓存中。加载输入和权重图块时支持动态填充。 +* `GEMM` 指令:在输入张量和权重张量上执行矩阵-矩阵乘法的微操作序列,并将结果添加到寄存器堆张量。 +* `ALU` 指令:对寄存器文件张量数据执行矩阵-矩阵 ALU 操作的微操作序列。 +* `STORE` 指令:将 2D 张量从输出缓冲区存储到 DRAM。 + +`LOAD` 指令由 load 和 compute 模块执行,具体取决于存储内存缓冲区位置 target。`GEMM` 和 `ALU` 指令由 compute 模块的 GEMM core 和张量 ALU 执行。最后,`STORE` 指令由 store 模块独占执行。每条指令的字段如下图所示。所有字段的含义将在 [微架构概述](https://tvm.apache.org/docs/topic/vta/dev/hardware.html#vta-uarch) 章节进一步解释。 + +![/img/docs/uwsampl/web-data/main/vta/developer/vta_instructions.png](/img/docs/uwsampl/web-data/main/vta/developer/vta_instructions.png) + +:::note +VTA ISA 会随着 VTA 的架构参数(即 GEMM core shape、数据类型、内存大小等)的修改而变化,因此 ISA 不能保证所有 VTA 变体的兼容性。但这是可以接受的,因为 VTA runtime 会适应参数变化,并生成和生成的加速器版本匹配的二进制代码。这体现了 VTA 堆栈采用的协同设计理念,它包含硬件-软件接口的流动性。 +::: + +### 数据流执行 + +VTA 依靠硬件模块之间依赖 FIFO 队列(dependence FIFO queues),来同步任务并发执行。下图展示了给定的硬件模块,如何用依赖 FIFO 队列和单读取器/单写入器 SRAM 缓冲区,以数据流的方式同时从其生产者和消费者模块执行。所有模块都通过写后读(RAW)和读后写(WAR)依赖队列连接到其消费者和生产者。 + +![/img/docs/uwsampl/web-data/main/vta/developer/dataflow.png](/img/docs/uwsampl/web-data/main/vta/developer/dataflow.png) + +以上伪代码描述了,模块如何基于与其他指令的依赖关系,执行给定指令。首先,每条指令中的依赖标志在硬件中被解码。若指令具有传入的 RAW 依赖,则基于从生产者模块接收到 RAW 依赖 token 执行。 + +类似地,若任务具有传入的 WAR 依赖,则基于从消费者模块接收到 WAR 依赖 token 执行。最后,任务完成后,检查输出的 RAW 和 WAR 依赖关系,并分别通知消费者和生产者模块。 + +:::note +此场景中的依赖 token 是无信息的。这是因为每个模块执行的指令是以 FIFO 顺序到达,无法按设计重新排序。 +::: + +### pipeline 可扩展性 + +默认的 VTA 设计由四个模块组成,它们描述了3 个阶段的 `load-compute-store` 任务 pipeline。遵循数据流硬件组织原则,可以扩展 VTA pipeline,使其包含更多阶段。 + +例如,设想将张量 ALU 与 GEMM core 分离,从而最大化利用 GEMM core。将产生一个密切反映 TPU 设计的 `load-gemm-activate-store` 任务 pipeline。然而,添加更多阶段是有代价的:它会增加存储和逻辑开销,这就是默认选择 3 个阶段的 pipeline 的原因。 + +## 微架构概述 + +本节描述了构成 VTA 设计的模块。模块定义包含在 `3rdparty/vta-hw/hardware/xilinx/sources/vta.cc` 中。 + +### Fetch 模块 + +VTA 由线性指令流编程。fetch 模块是 VTA 到 CPU 的入口点,通过三个内存映射寄存器进行编程: + +* 读写 `control` 寄存器启动 fetch 模块,然后读取它来检查其是否完成。 +* 只写 `insn_count` 寄存器设置要执行的指令数。 +* 只写 `insns` 寄存器设置 DRAM 中指令流的起始地址。 + +CPU 在 VTA runtime 分配的物理连续的 buffer 中,准备 DRAM 中的指令流。指令流就绪后,CPU 将起始物理地址写入 `insns` 寄存器,将指令流的长度写入 `insn_count` 寄存器,并在 `control` 寄存器中断言启动信号。此过程会启动 VTA(通过 DMA 从 DRAM 读取指令流)。 + +访问指令流时,fetch 模块会对指令部分解码,并将这些指令推送到命令队列中,这些指令队列会送到 load、compute 和 store 模块中: + +* `STORE` 指令被推送到存储命令队列,供 store 模块处理。 +* `GEMM` 和 `ALU` 指令被推送到计算命令队列,供 compute 模块处理。 +* 描述微操作内核或寄存器文件数据的加载操作的 `LOAD` 指令被推送到计算命令队列,供 compute 模块处理。 +* 描述输入或权重数据的加载操作的 `LOAD` 指令被推送到加载命令队列,供 load 模块处理。 + +当其中一个命令队列被填满,fetch 模块会停止,直到队列恢复未满状态。因此,命令队列的大小要足够大,允许较宽的执行窗口,还允许多个任务在 `load-compute-store` pipeline 中同时运行。 + +### Compute 模块 + +VTA 的 compute 模块充当 RISC 处理器,在张量寄存器(而非标量寄存器)上执行计算。两个功能单元改变寄存器文件:张量 ALU 和 GEMM core。 + +compute 模块从微操作缓存中取出 RISC 微操作执行。有两种类型的计算微操作:ALU 和 GEMM 操作。为了最小化微操作内核的占用空间,同时避免对控制流指令(如条件跳转)的需求,compute 模块在两级嵌套循环内执行微操作序列,该循环通过一个仿射函数计算每个张量寄存器的位置。这种压缩方法有助于减少微内核指令的占用空间,适用于矩阵乘法和 2D 卷积,这在神经网络算子中很常见。 + +![/img/docs/uwsampl/web-data/main/vta/developer/gemm_core.png](/img/docs/uwsampl/web-data/main/vta/developer/gemm_core.png) + +**GEMM core** 通过在 2 级嵌套循环(如上图所示)中执行微代码序列来评估 GEMM 指令。GEMM core 每个周期可以执行一次输入权重矩阵乘法。单周期矩阵乘法的维度定义了 TVM 编译器将计算 schedule 降级后得到的硬件*张量内联函数*。 + +这种张量内联函数由输入、权重和累加器张量的维度定义。每种数据类型的整数精度不同:通常权重和输入类型都是低精度(8 位或更少),而累加器张量具有更宽的类型(32 位),防止溢出。为了让 GEMM core 保持高利用率,每个输入缓冲区、权重缓冲区和寄存器文件都必须提供足够的读/写带宽。 + +![/img/docs/uwsampl/web-data/main/vta/developer/alu_core.png](/img/docs/uwsampl/web-data/main/vta/developer/alu_core.png) + +**Tensor ALU** 支持一组标准操作来实现常见的激活、归一化和池化操作。VTA 的设计遵循模块化原则,Tensor ALU 支持的算子范围可以进一步扩大,但代价是消耗更多的资源。 + +张量 ALU 可以执行张量-张量运算,以及对立即值执行张量-标量运算。张量 ALU 的操作码和立即数由高级 CISC 指令指定。张量 ALU 计算上下文中的微代码只负责指定数据访问模式。 + +:::note +在计算吞吐量方面,Tensor ALU 的执行速度不是每个周期一个操作。缺少读取端口会带来一些限制:由于每个周期可以读取一个寄存器文件张量,因此张量 ALU 的启动间隔至少为 2(即每 2 个周期最多执行 1 个操作)。 +::: + +此外,一次执行单个张量-张量操作可能会很耗时,尤其是寄存器文件类型很宽(wide),通常是 32 位整数。因此,为了平衡 Tensor ALU 与 GEMM 核心的资源利用率,默认情况下,张量-张量操作是通过多个周期的向量-向量操作来执行的。 + +### Load 和 Store 模块 + +![/img/docs/uwsampl/web-data/main/vta/developer/2d_dma.png](/img/docs/uwsampl/web-data/main/vta/developer/2d_dma.png) + +load 和 store 模块使用从 DRAM 到 SRAM 的跨步访问模式执行 2D DMA 加载。此外,load 模块可以动态插入 2D 填充(在阻塞 2D 卷积时很有用)。这意味着 VTA 可以平铺 2D 卷积输入,而无需补偿在 DRAM 中重新布局数据在输入和权重块(weight tiles)周围插入空间填充的开销。 diff --git a/versioned_docs/version-0.12.0/topic/vta/dev/index.md b/versioned_docs/version-0.12.0/topic/vta/dev/index.md new file mode 100644 index 00000000..6e14c8d2 --- /dev/null +++ b/versioned_docs/version-0.12.0/topic/vta/dev/index.md @@ -0,0 +1,12 @@ +--- +title: VTA 设计及开发者指南 +--- + +# VTA 设计及开发者指南 + +本开发者指南详细介绍了完整的 VTA-TVM 硬件-软件堆栈。 + +![/img/docs/uwsampl/web-data/main/vta/blogpost/vta_overview.png](/img/docs/uwsampl/web-data/main/vta/blogpost/vta_overview.png) + +* [VTA 配置](config) +* [VTA 硬件指南](hardware) diff --git a/versioned_docs/version-0.12.0/topic/vta/install.md b/versioned_docs/version-0.12.0/topic/vta/install.md new file mode 100644 index 00000000..7ede15c8 --- /dev/null +++ b/versioned_docs/version-0.12.0/topic/vta/install.md @@ -0,0 +1,413 @@ +--- +title: VTA 安装指南 +sidebar_position: 100 +--- + +# VTA 安装指南 + +我们提供了五个安装指南,每一个都对前一个教程进行了扩展: + +1. [VTA 模拟器安装](#vta-simulator-installation) +2. [Xilinx Pynq FPGA 设置](#xilinx-pynq-fpga-setup) +3. [Intel DE10 FPGA 设置](#intel-de10-fpga-setup) +4. [使用 Xilinx 工具链生成比特流](#bitstream-generation-with-xilinx-toolchains) +5. [使用 Intel 工具链生成比特流](#bitstream-generation-with-intel-toolchains) + +## VTA 模拟器安装 + +需要在机器上 [安装 TVM](/docs/install)。查看 [Docker 指南](/docs/install/docker) 来快速开始。 + +设置以下路径后才能使用 VTA: + +``` bash +export TVM_PATH= +export VTA_HW_PATH=$TVM_PATH/3rdparty/vta-hw +``` + +构建 TVM 时要启用 VTA 功能模拟库。 + +``` bash +cd +mkdir build +cp cmake/config.cmake build/. +echo 'set(USE_VTA_FSIM ON)' >> build/config.cmake +cd build && cmake .. && make -j4 +``` + +将 VTA Python 库添加到 Python 路径,运行 VTA 示例。 + +``` bash +export PYTHONPATH=/path/to/vta/python:${PYTHONPATH} +``` + +### 测试 VTA 模拟设置 + +为确保已正确安装 VTA Python 包,运行以下 2D 卷积进行测试。 + +``` bash +python /vta/tests/python/integration/test_benchmark_topi_conv2d.py +``` + +诚邀你体验 [VTA 编程教程](tutorials)。 + +**注意**:每个卷积层的吞吐量都会在 GOPS 中报告。这些数字实际上是模拟器通过评估软件中的卷积实现的计算吞吐量。 + +### 高级配置(可选) + +VTA 是一个通用的可配置深度学习加速器。配置由 `3rdparty/vta-hw/config` 下的 `vta_config.json` 指定。该文件提供了 VTA 加速器的体系结构规范,以参数化 TVM 编译器堆栈和 VTA 硬件堆栈。 + +VTA 配置文件还指定了 TVM 编译器 target。当 `TARGET` 设置为 `sim` 时,所有 TVM 工作负载都在 VTA 模拟器上执行。可以修改配置文件的内容,将 VTA 重建为不同的参数化: + +``` bash +cd +vim 3rdparty/vta-hw/config/vta_config.json +# 编辑 vta_config.json +make +``` + +## Xilinx Pynq FPGA 设置 + +第二个指南扩展了以上 *VTA 模拟器安装*指南,从而运行完整的 TVM 和 VTA 软件-硬件堆栈的 FPGA 硬件测试。需要的硬件组件有: + +* [Pynq](http://www.pynq.io/) FPGA 开发板可从 [Digilent](https://store.digilentinc.com/pynq-z1-python-productivity-for-zynq/) 以 200 美元或 150 美元的价格购买。 +* 一个以太网到 USB 适配器,用于将 Pynq 板连接到你的开发机器。 +* 8+ GB micro SD 卡。 +* 一个 AC 转 DC 12V 3A 电源适配器。 + +本指南涵盖以下主题: + +1. Pynq 板设置说明。 +2. Pynq 端 RPC 服务器构建和部署。 +3. 再次访问 *VTA 模拟器安装指南*中的测试示例,这次是在 Pynq 板上执行。 + +### Pynq 板设置 + +根据 [Pynq 开发板入门教程](http://pynq.readthedocs.io/en/latest/getting_started.html) 设置 Pynq 开发板。 + +按照说明进行操作,包括*打开 PYNQ-Z1* 步骤(此后无需继续学习本教程)。 + +* 确保已下载最新的 Pynq 镜像 [PYNQ-Z1 v2.5](http://www.pynq.io/board.html),并已经用它为你的 SD 卡制作镜像(推荐免费的 [Etcher](https://etcher.io/) 程序)。 +* 对于这个测试设置,遵循[“连接到计算机”](https://pynq.readthedocs.io/en/latest/getting_started/pynq_z1_setup.html)以太网设置说明。为成功与板子通信,确保 [为计算机分配一个静态 IP 地址](https://pynq.readthedocs.io/en/latest/appendix.html#assign-your-computer-a-static-ip)。 + +一旦开发板通电,并连接到你的开发机器,尝试进行连接,并确保已正确设置 Pynq 开发板: + +``` bash +# 要连接到 Pynq 开发板,使用 组合: +ssh xilinx@192.168.2.99 +``` + +### Pynq 端 RPC 服务器构建和部署 + +因为板到计算机的直接连接会阻止单板直接访问互联网,所以要使用 [sshfs](https://www.digitalocean.com/community/tutorials/how-to-use-sshfs-to-mount-remote-file-systems-over-ssh) 将 Pynq 的文件系统挂载到你的开发机器的文件系统中。接下来,直接将 TVM 仓库克隆到开发机器上的 sshfs 挂载点。 + +``` bash +# 在宿主机端 +mkdir +sshfs xilinx@192.168.2.99:/home/xilinx +cd +git clone --recursive https://github.com/apache/tvm tvm +# 完成后,可以离开挂载点,并卸载目录 +cd ~ +sudo umount +``` + +现在已经在 Pynq 的文件系统中克隆了 VTA 仓库,可以通过 ssh 登入,并基于 TVM 的 RPC 服务器启动构建。构建过程大约需要 5 分钟。 + +``` bash +ssh xilinx@192.168.2.99 +# 构建 TVM runtime 库(需要 5 分钟) +cd /home/xilinx/tvm +mkdir build +cp cmake/config.cmake build/. +echo 'set(USE_VTA_FPGA ON)' >> build/config.cmake +# 复制 pynq 具体配置 +cp 3rdparty/vta-hw/config/pynq_sample.json 3rdparty/vta-hw/config/vta_config.json +cd build +cmake .. +make runtime vta -j2 +# FIXME (tmoreau89): 通过修复 cmake 构建,删除此步骤 +make clean; make runtime vta -j2 +# 构建 VTA RPC 服务器(需要 1 分钟) +cd .. +sudo ./apps/vta_rpc/start_rpc_server.sh # pw is 'xilinx' +``` + +启动 RPC 服务器时,可看到以下显示。为了运行下一个示例,需要让 RPC 服务器在 `ssh` session 中运行。 + +``` bash +INFO:root:RPCServer: bind to 0.0.0.0:9091 +``` + +关于 Pynq RPC 服务器的提示: + +* RPC 服务器应该在端口 `9091` 上监听。若没有,早期的进程可能已经意外终止。在这种情况下推荐重新启动 Pynq,然后重新运行 RPC 服务器。 +* 要终止 RPC 服务器,只需发送 `Ctrl + c` 命令。可以用 `sudo ./apps/pynq_rpc/start_rpc_server.sh` 重新运行。 +* 若无响应,可以通过使用物理电源开关对其重新通电,重新启动单板。 + +### 测试基于 Pynq 的硬件设置 + +在开发机器上运行示例前,按如下方式配置主机环境: + +``` bash +# 在宿主机端 +export VTA_RPC_HOST=192.168.2.99 +export VTA_RPC_PORT=9091 +``` + +此外,还需将主机上的 `vta_config.json` 文件中 `TARGET` 字段设置为 `"pynq"` 来指定 target 是 Pynq 平台。 + +注意:与模拟设置相比,主机端没有要编译的库,因为主机会将所有计算转移到 Pynq 板上。 + +``` bash +# 在宿主机端 +cd +cp 3rdparty/vta-hw/config/pynq_sample.json 3rdparty/vta-hw/config/vta_config.json +``` + +运行 2D 卷积 testbench。在此之前,要用 VTA 比特流对 Pynq 板 FPGA 进行编程,并通过 RPC 构建 VTA runtime。以下 `test_program_rpc.py` 脚本将执行两个操作: + +* FPGA 编程,通过从 [VTA 比特流仓库](https://github.com/uwsampl/vta-distro) 中下载预编译的比特流,这个仓库与主机设置的默认配置 `vta_config.json` 匹配,并通过 RPC 发送到 Pynq,从而对 Pynq 的 FPGA 进行编程。 +* `vta_config.json` 配置每次修改,都要运行在 Pynq 上构建的 Runtime。这样可确保 VTA 软件 runtime(通过 just-in-time(JIT)编译生成加速器可执行文件)与在 FPGA 上编程的 VTA 设计规范匹配。构建过程大约需要 30 秒完成,耐心等待! + +``` bash +# 在宿主机端 +python /vta/tests/python/pynq/test_program_rpc.py +``` + +准备在硬件中运行 2D 卷积 testbench。 + +``` bash +# 在宿主机端 +python /vta/tests/python/integration/test_benchmark_topi_conv2d.py +``` + +每个卷积层在 Pynq 板上测试的性能指标都会生成报告。 + +**提示**:可以通过查看 Pynq `ssh` session 中 RPC 服务器的日志消息来跟踪 FPGA 编程和 runtime 重建步骤的进度。 + +更多信息请访问 [VTA 编程教程](tutorials)。 + +## Intel DE10 FPGA 设置 + +与 Pynq 端设置步骤类似,第三个指南详细介绍了如何为 Intel FPGA 板(如 DE10-Nano)设置 Linux 环境。 + +就硬件组件而言,需要 [DE10-Nano 开发套件](https://www.terasic.com.tw/cgi-bin/page/archive.pl?Language=English&No=1046),它可以从 [Terasic](https://www.terasic.com.tw/) 以 130 美元购入(教育优惠价格为 100 美元)。该套件提供一张 microSD 卡,以及电源线和 USB 线。但需要额外的网线将板连接到 LAN。 + +本指南将讲解以下步骤: + +* 用最新 Angstrom Linux 镜像烧录 microSD 卡 +* 交叉编译设置 +* 设备端 RPC 服务器设置和部署 + +### DE10-Nan 板设置 + +启动设备前,要用最新 Angstrom Linux 镜像烧录 microSD 卡镜像。 + +#### 烧录 SD 卡和引导 Angstrom Linux + +要在 DE10-Nano 上烧录 SD 卡,并启动 Linux,推荐查看 Terasic 公司的 DE10-Nano 产品页面的 [Resource](https://www.terasic.com.tw/cgi-bin/page/archive.pl?Language=English&CategoryNo=167&No=1046&PartNo=4) tab。在网页上注册并登录后,即可下载预构建的 Angstrom Linux 镜像并烧录。具体来说,要将下载的 Linux SD 卡镜像烧录到你的物理 SD 卡中: + +首先,提取 gzip 压缩的存档文件。 + +``` bash +tar xf de10-nano-image-Angstrom-v2016.12.socfpga-sdimg.2017.03.31.tgz +``` + +将生成一个名为 `de10-nano-image-Angstrom-v2016.12.socfpga-sdimg`(约 2.4 GB)的 SD 卡镜像,包含启动 Angstrom Linux 的所有文件系统。 + +其次,在你的 PC 中插入准备烧录的 SD 卡,并用 `fdisk -l` 查询磁盘的设备 ID,若觉得使用 GUI 更好,则用 `gparted`。磁盘的典型设备 ID 可能是 `/dev/sdb`。 + +然后,用以下命令将磁盘镜像烧录到你的物理 SD 卡中: + +``` bash +# 注意:运行以下命令通常需要 root 权限。 +dd if=de10-nano-image-Angstrom-v2016.12.socfpga-sdimg of=/dev/sdb status=progress +``` + +将整个文件系统写入 PC 的 SD 卡需要几分钟时间。完成后,就可以取下 SD 卡,并将其插入 DE10-Nano 板。接下来连接电源线和串口来启动 Angstrom Linux。 + +**注意**:从 microSD 卡启动时,可能会出现与 microSD 卡中 Linux 内核 `zImage` 不兼容的情况。这种情况下需要从 [linux-socfpga](https://github.com/altera-opensource/linux-socfpga) 仓库的 [socfpga-4.9.78-ltsi](https://github.com/altera-opensource/linux-socfpga/tree/socfpga-4.9.78-ltsi) 分支构建你自己的 `zImage` 文件。还可以 [从此链接](https://raw.githubusercontent.com/liangfu/de10-nano-supplement/master/zImage) 下载 `zImage` 文件的预构建版本来快速修复。 + +将 USB 线连接到 DE10-Nano 开发板后,连接电源线给开发板上电。然后,可以用主机 PC 上的 `minicom` 连接到设备的串行端口: + +``` bash +# 注意:运行以下命令通常需要 root 权限。 +minicom -D /dev/ttyUSB0 +``` + +设备的默认用户名为 `root`,默认密码为空。 + +接下来安装支持 Python3 的包(TVM 不再支持 Python2),具体来说是 `numpy`、`attrs` 和 `decorator`。 + +**注意**:在 DE10-Nano 设备上用 `pip3` 可能无法安装 `numpy`。这种情况可以从 [meta-de10-nano](https://github.com/intel/meta-de10-nano) 仓库为开发板构建文件系统镜像;也可以从现有的 Linux 发行版下载预构建的包,例如 Debian。我们已在 [此处](https://raw.githubusercontent.com/liangfu/de10-nano-supplement/master/rootfs_supplement.tgz) 连接了补充二进制文件,可以将文件提取到根文件系统来快速修复。 + +#### 安装所需的 Python 包 + +从串口访问 bash 终端后,要在构建和安装 TVM 和 VTA 程序之前,安装所需的 Python 包。 + +#### 构建附加组件以使用 VTA 比特流 + +要在 DE10-Nano 硬件上使用上述构建的比特流,需要为系统编译几个附加组件。具体来说,要为系统编译应用程序可执行文件,需要下载并安装 [SoCEDS](http://fpgasoftware.intel.com/soceds/18.1/?edition=standard&download_manager=dlm3&platform=linux)(推荐),或者在主机上安装 `g++-arm-linux-gnueabihf` 包。还需要一个 `cma` 内核模块,来分配连续内存,以及一个用于与 VTA 子系统通信的驱动程序。 + +## 使用 Xilinx 工具链生成比特流 + +若对自行生成 Xilinx FPGA 比特流感兴趣,而非直接使用预构建的 VTA 比特流,请按照以下说明进行操作。 + +### Xilinx 工具链安装 + +推荐使用 Vivado 2020.1,因为我们的脚本已经通过测试,可以在此版本的 Xilinx 工具链上运行。本指南是为 Linux(Ubuntu)安装编写的。 + +需要安装 Xilinx 的 FPGA 编译工具链 [Vivado HL WebPACK 2020.1](https://www.xilinx.com/products/design-tools/vivado.html),它是 Vivado HLx 工具链的免许可版本。 + +#### 获取并启动 Vivado GUI 安装程序 + +1. 访问 [下载网页](https://www.xilinx.com/support/download/index.html/content/xilinx/en/downloadNav/vivado-design-tools/2020-1.html),下载适用于 Vivado HLx 2020.1 的 Linux 自解压 Web 安装程序:WebPACK 和 Editions。 +2. 必须用 Xilinx 帐户登录。需要约 2 分钟创建一个 Xilinx 帐户。 +3. 单击「Next」完成名称和地址验证,然后可以下载名为 `Xilinx_Unified_2020.1_0602_1208_Lin64.bin` 的二进制文件。 +4. 文件下载后,在你的 `Downloads` 目录下更改文件权限,然后就可以执行: + + ``` bash + chmod u+x Xilinx_Unified_2020.1_0602_1208_Lin64.bin + ``` + +5. 现在可以执行二进制文件: + + ``` bash + ./Xilinx_Unified_2020.1_0602_1208_Lin64.bin + ``` + +#### Xilinx Vivado GUI 安装程序步骤 + +此时已启动 Vivado 2020.1 安装 GUI 程序。 + + 1. 在「Welcome」界面,单击「Next」。 + 2. 在「Select Install Type」界面的「User Authentication」框中,输入你的 Xilinx 用户凭据,然后选择「Download and Install Now」选项,单击「Next」。 + 3. 在「Accept License Agreements」界面上,接受所有条款,然后单击「Next」。 + 4. 在「Select Edition to Install」界面,选择「Vivado HL WebPACK」,然后单击「Next」。 + 5. 在「Vivado HL WebPACK」界面,在点击「Next」之前,检查以下选项(其余选项应取消选中): * Design Tools -> Vivado Design Suite -> Vivado * Devices -> Production Devices -> SoCs -> Zynq -7000(若 target 是 Pynq 板)* Devices -> Production -> SoC -> UltraScale+ MPSoC(若 target 是 Ultra-96 板) + 6. 总下载大小约为 5 GB,所需的磁盘空间量为 23 GB。 + 7. 在「Select Destination Directory」界面,设置安装目录,然后单击「下一步」。某些路径可能会突出显示为红色——这是因为安装程序没有写入目录的权限。在这种情况下,选择不需要特殊写入权限的路径(例如你的主目录)。 + 8. 在「Installation Summary」界面,点击「Install」。 + 9. 将弹出「Installation Progress」窗口,来跟踪下载和安装的进度。 +10. 此过程大约需要 20-30 分钟,具体取决于网络速度。 +11. 安装成功完成会弹出窗口通知。单击「OK」。 +12. 最后,「Vivado License Manager」将启动。选择「Get Free ISE WebPACK, ISE/Vivado IP or PetaLinux License」,并单击「Connect Now」,完成许可证注册过程。 + +#### 环境设置 + +最后一步是用以下代码更新你的 `~/.bashrc`。这包括所有 Xilinx 二进制路径,以便可以从命令行启动编译脚本。 + +``` bash +# Xilinx Vivado 2020.1 环境 +export XILINX_VIVADO=${XILINX_PATH}/Vivado/2020.1 +export PATH=${XILINX_VIVADO}/bin:${PATH} +``` + +### Pynq 基于 HLS 的自定义 VTA 比特流的编译 + +用户可自定义 VTA 配置文件中的高级硬件参数。尝试自定义 VTA 比特流编译时,可将时钟频率调快一点。 + +* 将 `HW_FREQ` 字段设置为 `142`。Pynq 板支持 100、142、167 和 200MHz 时钟频率。注意,频率越高,关闭时序就越困难。增加频率会导致时序违规,从而导致硬件执行错误。 +* 将 `HW_CLK_TARGET` 设置为 `6`。这个参数指的是 HLS 的目标时钟周期(以纳秒为单位)——较低的时钟周期会导致流水线操作更快,从而在较高频率下实现时序收敛。从技术上讲,142MHz 的时钟需要 7ns 的目标机,但我们刻意将时钟目标机降低到 6ns,从而更好地将设计流水线化。 + +比特流的生成是由 `/3rdparty/vta-hw/hardware/xilinx/` 顶层目录的 `Makefile` 驱动的。 + +若只想在软件仿真中模拟 VTA 设计,确保其正常工作,输入: + +``` bash +cd /3rdparty/vta-hw/hardware/xilinx +make ip MODE=sim +``` + +若只想生成基于 HLS 的 VTA IP 内核,而不启动整个设计布局和布线,输入: + +``` bash +make ip +``` + +然后就可以在 `/3rdparty/vta-hw/build/hardware/xilinx/hls///solution0/syn/report/_csynth.rpt` 下查看 HLS 综合报告。 + +**注意**:`` 的名称是一个字符串,它汇总了 `vta_config.json` 中列出的 VTA 配置参数。`` 的名称指的是构成高级 VTA 管道的特定模块(或 HLS 函数)。 + +最后,执行 `make` 命令来运行完整的硬件编译,并生成 VTA 比特流。 + +这个过程很长,大约一个小时才能完成,具体时长取决于机器的规格。建议在 Makefile 中设置 `VTA_HW_COMP_THREADS` 变量,充分利用开发机器上的所有内核。 + +编译后,可以在 `/3rdparty/vta-hw/build/hardware/xilinx/vivado//export/vta.bit` 下找到生成的比特流。 + +### 使用自定义比特流 + +可以通过在教程示例或 `test_program_rpc.py` 脚本中设置 `vta.program_fpga()` 函数的比特流路径,来对新的 VTA FPGA 比特流进行编程。 + +``` python +vta.program_fpga(remote, bitstream="/3rdparty/vta-hw/build/hardware/xilinx/vivado//export/vta.bit") +``` + +TVM 不会从 VTA 比特流仓库中下载预构建的比特流,而是用生成的新比特流,这是一种时钟频率更高的 VTA 设计。是否有观察到 ImageNet 分类示例的性能显著提高? + +## 使用 Intel 工具链生成比特流 + +若对自行生成 Xilinx FPGA 比特流感兴趣,而非直接使用预构建的 VTA 比特流,按照以下说明进行操作。 + +### Intel 工具链安装 + +推荐使用 `Intel Quartus Prime 18.1`,因为本文档中包含的测试脚本已经在该版本上进行了测试。 + +需要安装 Intel 的 FPGA 编译工具链 [Quartus Prime Lite](http://fpgasoftware.intel.com/?edition=lite),它是 Intel Quartus Prime 软件的免许可版本。 + +#### 获取和启动 Quartus GUI 安装程序 + +1. 访问 [下载中心](http://fpgasoftware.intel.com/?edition=lite),在「Separate file」选项卡中下载 Linux 版本的「Quartus Prime(include Nios II EDS)」和「Cyclone V device support」。这样可以避免下载未使用的设备支持文件。 +2. 若有帐户,填写表单登录;或在网页右侧注册帐户。 +3. 登录后,可以下载安装程序和设备支持文件。 +4. 文件下载后,在你的 `Downloads` 目录下更改文件权限: + + ``` bash + chmod u+x QuartusLiteSetup-18.1.0.625-linux.run + ``` + +5. 现在确保安装程序和设备支持文件都在同一目录中,用以下命令运行安装: + + ```plain + ./QuartusLiteSetup-18.1.0.625-linux.run + ``` + +6. 按照弹出的 GUI 表单上的说明,将所有内容安装在 `/usr/local` 目录中。安装后,会创建 `/usr/local/intelFPGA_lite/18.1` 文件夹,包含 Quartus 程序和其他程序。 + +#### 环境设置 + +与 Xilinx 工具链的操作类似,将以下代码添加到 `~/.bashrc` 中。 + +``` bash +# Intel Quartus 18.1 环境 +export QUARTUS_ROOTDIR="/usr/local/intelFPGA_lite/18.1/quartus" +export PATH=${QUARTUS_ROOTDIR}/bin:${PATH} +export PATH=${QUARTUS_ROOTDIR}/sopc_builder/bin:${PATH} +``` + +quartus 二进制路径会添加到 `PATH` 环境变量中,然后可以从命令行启动编译脚本。 + +### DE10-Nano 基于 Chisel 的自定义 VTA 比特流的编译 + +与基于 HLS 的设计类似,用户可以自定义 VTA 配置文件 [Configs.scala](https://github.com/apache/tvm/blob/main/3rdparty/vta-hw/hardware/chisel/src/main/scala/core/Configs.scala) 中基于 Chisel 设计的高级硬件参数。 + +对于 Intel FPGA,比特流的生成是由 `/3rdparty/vta-hw/hardware/intel` 顶级目录下的 `Makefile` 驱动的。 + +若只为 DE10-Nano 板生成基于 Chisel 的 VTA IP 核,而不为 FPGA 硬件编译设计,输入: + +``` bash +cd /3rdparty/vta-hw/hardware/intel +make ip +``` + +然后,就可以在 `/3rdparty/vta-hw/build/hardware/intel/chisel//VTA.DefaultDe10Config.v` 中找到生成的 verilog 文件。 + +若要为 `de10nano` 板运行完整的硬件编译: + +``` bash +make +``` + +这个过程可能会有点长,需要半小时才能完成,具体时长取决于 PC 的性能。Quartus Prime 软件会自动检测 PC 上可用的内核数量,并用所有内核来执行此过程。 + +编译后,可以在 `/3rdparty/vta-hw/build/hardware/intel/quartus//export/vta.rbf` 下找到生成的比特流。还可以打开 `/3rdparty/vta-hw/build/hardware/intel/quartus//de10_nano_top.qpf` 路径的 Quartus 项目文件(.qpf),查看生成的报告。 diff --git a/versioned_docs/version-0.12.0/topic/vta/tutorials/01-mat_mul.md b/versioned_docs/version-0.12.0/topic/vta/tutorials/01-mat_mul.md new file mode 100644 index 00000000..90daef2e --- /dev/null +++ b/versioned_docs/version-0.12.0/topic/vta/tutorials/01-mat_mul.md @@ -0,0 +1,490 @@ +--- +title: 简单矩阵乘法 +--- + +# 简单矩阵乘法 + +:::note +单击 [此处](https://tvm.apache.org/docs/topic/vta/tutorials/matrix_multiply.html#sphx-glr-download-topic-vta-tutorials-matrix-multiply-py) 下载完整的示例代码 +::: + +**作者**:[Thierry Moreau](https://homes.cs.washington.edu/\~moreau/) + +本教程以 [VTA 入门](start_vta) 教程为基础,并介绍用 TVM workflow 在 VTA 上实现矩阵乘法所需的其他概念。 + +## RPC 设置 + +首先对 Pynq 的 FPGA 进行编程,并构建其 RPC runtime,步骤与 VTA 入门教程类似。 + +``` python +from __future__ import absolute_import, print_function + +import os +import tvm +from tvm import te +import vta +import numpy as np +from tvm import rpc +from tvm.contrib import utils +from vta.testing import simulator + +# 从 3rdparty/vta-hw/config/vta_config.json 文件加载 VTA 参数 +env = vta.get_env() + +# 从 OS 环境中读取 Pynq RPC 主机 IP 地址和端口号 +host = os.environ.get("VTA_RPC_HOST", "192.168.2.99") +port = int(os.environ.get("VTA_RPC_PORT", "9091")) + +# 在 Pynq 上配置比特流和 runtime 系统 +# 匹配 vta_config.json 文件指定的 VTA 配置。 +if env.TARGET == "pynq" or env.TARGET == "de10nano": + # 确保 TVM 是使用 RPC=1 编译的 + assert tvm.runtime.enabled("rpc") + remote = rpc.connect(host, port) + + # 重新配置 JIT runtime + vta.reconfig_runtime(remote) + + # 使用预编译的 VTA 比特流对 FPGA 进行编程。 + # 可以通过传递比特流文件的路径 + # 使用自定义比特流而非 None对 FPGA 进行编程。 + vta.program_fpga(remote, bitstream=None) + +# 在模拟模式下,本地托管 RPC 服务器。 +elif env.TARGET in ["sim", "tsim"]: + remote = rpc.LocalSession() +``` + +## 计算声明 + +示例中描述了一个简单的矩阵乘法加法,它需要多个计算阶段,如下面的数据流图所示。首先,描述主存储器中的输入张量 `A` 和 `B`。然后,声明 VTA 的芯片上缓冲区的中间张量 `A_buf` 和 `B_buf`。这个额外的计算阶段使得能够显式地暂存缓存的读和写。第三,描述对 `A_buf` 和 `B_buf` 的矩阵乘法计算,产生乘积矩阵 `C_buf`。最后一个操作是,强制转换并拷贝回 DRAM,进入结果张量 `C`。 + +![/img/docs/uwsampl/web-data/main/vta/tutorial/gemm_dataflow.png](/img/docs/uwsampl/web-data/main/vta/tutorial/gemm_dataflow.png) + +### 数据布局 + +以平铺数据格式描述占位符张量 `A` 和 `B`,匹配 VTA 张量 core 提出的数据布局要求。 + +:::note +**数据平铺** + +目标加速器之所以复杂,一个很重要的原因就是需要确保数据布局与加速器设计要求的布局相匹配。VTA 的设计以*张量 core*为核心,它在激活矩阵和权重矩阵之间每个周期执行一次矩阵-矩阵运算,将结果矩阵添加到累加器矩阵,如下图所示。 + +![/img/docs/uwsampl/web-data/main/vta/tutorial/tensor_core.png](/img/docs/uwsampl/web-data/main/vta/tutorial/tensor_core.png) + +在 `vta_config.json` 配置文件中指定该矩阵-矩阵乘法的维度。激活矩阵的 shape 为 `(BATCH, BLOCK_IN)`,转置权重矩阵的 shape 为 `(BLOCK_OUT, BLOCK_IN)`,因此推断生成的输出矩阵的 shape 为 `(BATCH, BLOCK_OUT)`。因此,VTA 处理的输入和输出张量要根据上述维度进行平铺。 + +下图展示了数据平铺对初始 shape 为 (4, 8) 的矩阵的影响。按 (2, 2) shape 进行平铺可确保每个平铺内的数据是连续的。生成的平铺张量的 shape 为 (2, 4, 2, 2)。 + +![/img/docs/uwsampl/web-data/main/vta/tutorial/data_tiling.png](/img/docs/uwsampl/web-data/main/vta/tutorial/data_tiling.png) +::: + +首先定义变量 `m`、`n`、`o` 来表示矩阵乘法的 shape。这些变量分别是 `BLOCK_OUT`、`BLOCK_IN` 和 `BATCH` 张量维度的乘法因子。配置文件默认将 `BATCH`、`BLOCK_IN` 和 `BLOCK_OUT` 分别设置为 1、16 和 16(`BATCH` 设置为 1 意味着计算构建块是向量矩阵乘法)。 + +:::note +**数据类型** + +不仅要匹配 VTA 张量 core 的内部平铺维度,还要匹配 VTA 期望的特定数据类型。VTA 目前仅支持固定数据类型,整数宽度在 `vta_config.json` 文件中指定,`INP_WIDTH` 和 `WGT_WIDTH` 分别指定激活和权重数据类型。此外,累加器数据类型的整数宽度由 `ACC_WIDTH` 指定。 +::: + +配置文件默认将 `INP_WIDTH` 和 `WGT_WIDTH` 设置为 8。将累加器宽度 `ACC_WIDTH` 设置为 32,避免累加时溢出。因此,`env.inp_dtype` 和 `env.wgt_dtype` 都是 8 位窄整数,而 `env.acc_dtype` 是标准的 32 位整数。 + +``` python +# 输出通道因子 m - 总共 16x16=256 输出通道 +m = 16 +# 输入通道因子 n - 总共 16x16=256 输入通道 +n = 16 +# Batch 因子 o (我们使用单一 batch 推断) +o = 1 +# A 平铺数据格式的占位符张量 +A = te.placeholder((o, n, env.BATCH, env.BLOCK_IN), name="A", dtype=env.inp_dtype)) +# B 平铺数据格式的占位符张量 +B = te.placeholder((m, n, env.BLOCK_OUT, env.BLOCK_IN), name="B", dtype=env.wgt_dtype)) +# A 复制缓冲区 +A_buf = te.compute((o, n, env.BATCH, env.BLOCK_IN), lambda *i: A(*i), "A_buf")) +# B 复制缓冲区 +B_buf = te.compute((m, n, env.BLOCK_OUT, env.BLOCK_IN), lambda *i: B(*i), "B_buf")) +``` + +### 矩阵乘法 + +用另一个计算操作来描述矩阵乘法结果张量 `C`。计算函数采用张量的 shape,以及描述张量每个位置的计算规则的 lambda 函数。 + +为实现矩阵乘法,lambda 函数要在输入通道维度轴上包含一个归约公式。要创建归约公式,可以用 `te.reduce_axis` 声明归约轴,它接收归约范围。`te.sum` 接收要归约的表达式以及归约轴,来计算声明范围内所有 k 值的总和。 + +注意,要对 32 位 `env.acc_dtype` 累加器数据类型执行归约。 + +这个阶段只声明如何进行计算,不会发生任何计算。 + +``` python +# 外部输入特征 reduction 轴 +ko = te.reduce_axis((0, n), name="ko") +# 内部输入特征 reduction 轴 +ki = te.reduce_axis((0, env.BLOCK_IN), name="ki") +# 描述 in-VTA 矩阵乘法 +C_buf = te.compute( + (o, m, env.BATCH, env.BLOCK_OUT), + lambda bo, co, bi, ci: te.sum( + A_buf[bo, ko, bi, ki].astype(env.acc_dtype) * B_buf[co, ko, ci, ki].astype(env.acc_dtype), + axis=[ko, ki], + ), + name="C_buf", +) +``` + +### 转换结果 + +计算完成后,将 VTA 计算的结果送回主存。 + +:::note +**内存存储限制** + +VTA 的一个特点是它只支持窄的 `env.inp_dtype` 数据类型格式的 DRAM 存储。这可以减少内存传输的数据占用时间,还可以将宽累加器数据类型量化为与输入激活数据类型匹配的数据格式。这意味着在神经网络推理的上下文中,激活后给定层的输出可以直接被下一层利用。 +::: + +对窄的输入激活数据格式执行最后一个类型转换操作。 + +``` python +# 转换为输出类型,并送到主存 +C = te.compute( + (o, m, env.BATCH, env.BLOCK_OUT), lambda *i: C_buf(*i).astype(env.inp_dtype), name="C" +) +``` + +本教程的计算声明部分到此结束。 + +## 调度计算 + +虽然上面描述了计算规则,但可以通过多种方式获得 `C`。 TVM 要求用户提供名为 *schedule* 的计算实现。 + +schedule 是对原始计算的一组转换,它在不影响正确性的情况下转换计算的实现。这个简单的 VTA 编程教程旨在演示将原始 schedule 映射到 VTA 硬件原语的基本 schedule 转换。 + +### 默认 schedule + +构建 schedule 后,schedule 默认以下面的方式计算 `C`: + +``` python +# 生成的 schedule +s = te.create_schedule(C.op) +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(int8), int8, [256], []), + B: Buffer(B_2: Pointer(int8), int8, [65536], []), + C: Buffer(C_2: Pointer(int8), int8, [256], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, int8, [1, 16, 1, 16], []), B_1: B_3: Buffer(B_2, int8, [16, 16, 16, 16], []), C_1: C_3: Buffer(C_2, int8, [1, 16, 1, 16], [])} { + allocate(A_buf: Pointer(global int8), int8, [256]), storage_scope = global; + allocate(B_buf: Pointer(global int8), int8, [65536]), storage_scope = global; + allocate(C_buf: Pointer(global int32), int32, [256]), storage_scope = global { + for (i1: int32, 0, 16) { + for (i3: int32, 0, 16) { + let cse_var_1: int32 = ((i1*16) + i3) + A_buf_1: Buffer(A_buf, int8, [256], [])[cse_var_1] = A[cse_var_1] + } + } + for (i0: int32, 0, 16) { + for (i1_1: int32, 0, 16) { + for (i2: int32, 0, 16) { + for (i3_1: int32, 0, 16) { + let cse_var_2: int32 = ((((i0*4096) + (i1_1*256)) + (i2*16)) + i3_1) + B_buf_1: Buffer(B_buf, int8, [65536], [])[cse_var_2] = B[cse_var_2] + } + } + } + } + for (co: int32, 0, 16) { + for (ci: int32, 0, 16) { + C_buf_1: Buffer(C_buf, int32, [256], [])[((co*16) + ci)] = 0 + for (ko: int32, 0, 16) { + for (ki: int32, 0, 16) { + let cse_var_3: int32 = ((co*16) + ci) + C_buf_1[cse_var_3] = (C_buf_1[cse_var_3] + (cast(int32, A_buf_1[((ko*16) + ki)])*cast(int32, B_buf_1[((((co*4096) + (ko*256)) + (ci*16)) + ki)]))) + } + } + } + } + for (i1_2: int32, 0, 16) { + for (i3_2: int32, 0, 16) { + let cse_var_4: int32 = ((i1_2*16) + i3_2) + C[cse_var_4] = cast(int8, C_buf_1[cse_var_4]) + } + } + } +} +``` + +尽管这个 schedule 有意义,但它不能编译为 VTA。为获得正确的代码生成,要应用调度原语和代码注释,将 schedule 转换为可以直接降级到 VTA 硬件内联函数的 schedule。包括: + +* DMA 复制操作将采用全局范围的张量,并将其复制到局部范围的张量中。 +* 执行矩阵乘法的张量运算。 + +### 缓冲区范围 + +首先,设置缓冲区的范围,告诉 TVM 这些缓冲区将存在于 VTA 芯片上的 SRAM 缓存中。接下来告诉 TVM,`A_buf`、`B_buf`、`C_buf` 将分别存在于 VTA 芯片上输入、权重和累加器内存中。 + +:::note +**VTA 芯片上的 SRAM** + +VTA 具有三个不同的内存范围,每个范围对应于不同芯片上 SRAM 缓冲区。 + +* `env.inp_scope`:输入缓冲区,这是一个只读的 SRAM 缓冲区,用于存储 `env.inp_dtype` 类型的 shape 为 `(env.BATCH、env.BLOCK_IN)` 的输入矩阵。输入缓冲区包含 *2 ^ LOG_INP_BUFF_SIZE* 矩阵元素(在 `vta_config.json` 文件中指定)。 +* `env.wgt_scope`:权重缓冲区,这是一个只读的 SRAM 缓冲区,用于存储 `env.wgt_dtype` 类型的 shape 为 `(env.BLOCK_OUT,env.BLOCK_IN)` 的权重矩阵。权重缓冲区包含 *2 ^ LOG_WGT_BUFF_SIZE* 矩阵元素。 +* `env.acc_scope`:累加器缓冲区,它是一个读/写 SRAM 缓冲区,用于存储 `env.acc_dtype` 类型的 shape 为 `(env.BATCH, env.BLOCK_OUT)` 的累加器矩阵。累加器缓冲区是 VTA 的通用寄存器文件:它保存卷积和矩阵乘法的中间结果以及池化、批量归一化和激活层的中间结果。累加器缓冲区包含 *2 ^ LOG_ACC_BUFF_SIZE* 矩阵元素。 +::: + +``` python +# 将中间张量的范围设置为 VTA 的芯片上缓冲区 +s[A_buf].set_scope(env.inp_scope) +s[B_buf].set_scope(env.wgt_scope) +s[C_buf].set_scope(env.acc_scope) +``` + +输出结果: + +``` bash +stage(C_buf, compute(C_buf, body=[reduce(combiner=comm_reducer(result=[(x + y)], lhs=[x], rhs=[y], identity_element=[0]), source=[(int32(A_buf[bo, ko, bi, ki])*int32(B_buf[co, ko, ci, ki]))], init=[], axis=[iter_var(ko, range(min=0, ext=16)), iter_var(ki, range(min=0, ext=16))], where=(bool)1, value_index=0)], axis=[iter_var(bo, range(min=0, ext=1)), iter_var(co, range(min=0, ext=16)), iter_var(bi, range(min=0, ext=1)), iter_var(ci, range(min=0, ext=16))], reduce_axis=[iter_var(ko, range(min=0, ext=16)), iter_var(ki, range(min=0, ext=16))], tag=, attrs={})) +``` + +### DMA 传输 + +调度 DMA 传输,将 DRAM 中的数据移入和移出 VTA 芯片上的缓冲区。可以用 `compute_at` 调度原语来实现,该原语将缓冲区的复制嵌套到执行矩阵乘法的计算循环中。 + +插入 `dma_copy` 编译指示,向编译器表明复制操作将通过 DMA 批量执行,这在硬件加速器中很常见。最后,打印临时 schedule,观察将复制操作移动到矩阵乘法循环中的效果。 + +``` python +# 将缓冲区副本移动到矩阵乘法循环中 +s[A_buf].compute_at(s[C_buf], ko) +s[B_buf].compute_at(s[C_buf], ko) + +# 使用 DMA pragma 标记缓冲区拷贝,插入 DMA 传输 +s[A_buf].pragma(s[A_buf].op.axis[0], env.dma_copy) +s[B_buf].pragma(s[B_buf].op.axis[0], env.dma_copy) +s[C].pragma(s[C].op.axis[0], env.dma_copy) + +# 查看转换后的 schedule +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(int8), int8, [256], []), + B: Buffer(B_2: Pointer(int8), int8, [65536], []), + C: Buffer(C_2: Pointer(int8), int8, [256], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, int8, [1, 16, 1, 16], []), B_1: B_3: Buffer(B_2, int8, [16, 16, 16, 16], []), C_1: C_3: Buffer(C_2, int8, [1, 16, 1, 16], [])} { + allocate(C_buf: Pointer(local.acc_buffer int32), int32, [256]), storage_scope = local.acc_buffer; + allocate(A_buf: Pointer(local.inp_buffer int8), int8, [16]), storage_scope = local.inp_buffer; + allocate(B_buf: Pointer(local.wgt_buffer int8), int8, [16]), storage_scope = local.wgt_buffer { + for (co: int32, 0, 16) { + for (ci: int32, 0, 16) { + C_buf_1: Buffer(C_buf, int32, [256], [], scope="local.acc_buffer", align=16)[((co*16) + ci)] = 0 + for (ko: int32, 0, 16) { + attr [IterVar(i0: int32, (nullptr), "DataPar", "")] "pragma_dma_copy" = 1; + for (i3: int32, 0, 16) { + A_buf_1: Buffer(A_buf, int8, [16], [], scope="local.inp_buffer", align=16)[i3] = A[((ko*16) + i3)] + } + attr [IterVar(i0_1: int32, (nullptr), "DataPar", "")] "pragma_dma_copy" = 1; + for (i3_1: int32, 0, 16) { + B_buf_1: Buffer(B_buf, int8, [16], [], scope="local.wgt_buffer", align=256)[i3_1] = B[((((co*4096) + (ko*256)) + (ci*16)) + i3_1)] + } + for (ki: int32, 0, 16) { + let cse_var_1: int32 = ((co*16) + ci) + C_buf_1[cse_var_1] = (C_buf_1[cse_var_1] + (cast(int32, A_buf_1[ki])*cast(int32, B_buf_1[ki]))) + } + } + } + } + attr [IterVar(i0_2: int32, (nullptr), "DataPar", "")] "pragma_dma_copy" = 1; + for (i1: int32, 0, 16) { + for (i3_2: int32, 0, 16) { + let cse_var_2: int32 = ((i1*16) + i3_2) + C[cse_var_2] = cast(int8, C_buf_1[cse_var_2]) + } + } + } +} +``` + +### 张量化 + +schedule 转换的最后一步是,将*张量化*应用于 schedule。张量化类似于向量化,只不过将概念扩展到更高维的计算单元。因此,张量化会在声明数据布局输入占位符时,规定数据布局约束,如前所述。我们已经用平铺格式排列了张量,因此接下来对循环重新排序,使其适应张量化。 + +持续将最外面的 reduction 轴移出。首先要迭代输入通道,然后是 batch 维度,最后是输出通道。最后,沿最内层矩阵-矩阵乘法张量块的外轴应用张量调度原语 `tensorize`。打印最终的 schedule(即将由 VTA runtime JIT 编译器生成代码): + +``` python +s[C_buf].reorder( + ko, s[C_buf].op.axis[0], s[C_buf].op.axis[1], s[C_buf].op.axis[2], s[C_buf].op.axis[3], ki +) +s[C_buf].tensorize(s[C_buf].op.axis[2], env.gemm) + +# 查看最终确定的 schedule +print(vta.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(int8), int8, [256], []), + B: Buffer(B_2: Pointer(int8), int8, [65536], []), + C: Buffer(C_2: Pointer(int8), int8, [256], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, int8, [1, 16, 1, 16], []), B_1: B_3: Buffer(B_2, int8, [16, 16, 16, 16], []), C_1: C_3: Buffer(C_2, int8, [1, 16, 1, 16], [])} { + attr [IterVar(vta: int32, (nullptr), "ThreadIndex", "vta")] "coproc_scope" = 2 { + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_uop_scope" = "VTAPushGEMMOp" { + @tir.call_extern("VTAUopLoopBegin", 16, 1, 0, 0, dtype=int32) + @tir.vta.uop_push(0, 1, 0, 0, 0, 0, 0, 0, dtype=int32) + @tir.call_extern("VTAUopLoopEnd", dtype=int32) + } + @tir.vta.coproc_dep_push(2, 1, dtype=int32) + } + for (ko: int32, 0, 16) { + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_scope" = 1 { + @tir.vta.coproc_dep_pop(2, 1, dtype=int32) + @tir.call_extern("VTALoadBuffer2D", @tir.tvm_thread_context(@tir.vta.command_handle(, dtype=handle), dtype=handle), A_2, ko, 1, 1, 1, 0, 0, 0, 0, 0, 2, dtype=int32) + @tir.call_extern("VTALoadBuffer2D", @tir.tvm_thread_context(@tir.vta.command_handle(, dtype=handle), dtype=handle), B_2, ko, 1, 16, 16, 0, 0, 0, 0, 0, 1, dtype=int32) + @tir.vta.coproc_dep_push(1, 2, dtype=int32) + } + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_scope" = 2 { + @tir.vta.coproc_dep_pop(1, 2, dtype=int32) + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_uop_scope" = "VTAPushGEMMOp" { + @tir.call_extern("VTAUopLoopBegin", 16, 1, 0, 1, dtype=int32) + @tir.vta.uop_push(0, 0, 0, 0, 0, 0, 0, 0, dtype=int32) + @tir.call_extern("VTAUopLoopEnd", dtype=int32) + } + @tir.vta.coproc_dep_push(2, 1, dtype=int32) + } + } + @tir.vta.coproc_dep_push(2, 3, dtype=int32) + @tir.vta.coproc_dep_pop(2, 1, dtype=int32) + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_scope" = 3 { + @tir.vta.coproc_dep_pop(2, 3, dtype=int32) + @tir.call_extern("VTAStoreBuffer2D", @tir.tvm_thread_context(@tir.vta.command_handle(, dtype=handle), dtype=handle), 0, 4, C_2, 0, 16, 1, 16, dtype=int32) + } + @tir.vta.coproc_sync(, dtype=int32) +} +``` + +本教程的调度部分到此结束。 + +## TVM 编译 + +在完成指定 schedule 后,可将其编译为 TVM 函数。 + +``` python +# 构建 GEMM VTA 内核 +my_gemm = vta.build( + s, [A, B, C], tvm.target.Target("ext_dev", host=env.target_host), name="my_gemm" +) + +# 将编译后的模块写入目标文件。 +temp = utils.tempdir() +my_gemm.save(temp.relpath("gemm.o")) + +# 通过 RPC 发送可执行文件 +remote.upload(temp.relpath("gemm.o")) + +# 加载编译好的模块 +f = remote.load_module("gemm.o") +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:267: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +## 运行函数 + +编译好的 TVM 函数使用简洁的 C API,可以从代码语言中调用。 + +TVM 在 Python 中提供了一个基于 [DLPack](https://github.com/dmlc/dlpack) 标准的数组 API,帮助快速测试和原型设计。 + +* 首先创建一个远程 context(用于在 Pynq 上远程执行)。 +* 然后 `tvm.nd.array` 相应地格式化数据。 +* `f()` 运行实际计算。 +* `numpy()` 以可解释的格式复制结果数组。 + +``` python +# 获取远程设备上下文 +ctx = remote.ext_dev(0) + +# 在 (-128, 128] 的 int 范围内随机初始化 A 和 B 数组 +A_orig = np.random.randint(-128, 128, size=(o * env.BATCH, n * env.BLOCK_IN)).astype(A.dtype) +B_orig = np.random.randint(-128, 128, size=(m * env.BLOCK_OUT, n * env.BLOCK_IN)).astype(B.dtype) + +# 将 A 和 B 数组从 2D 打包为 4D 打包布局 +A_packed = A_orig.reshape(o, env.BATCH, n, env.BLOCK_IN).transpose((0, 2, 1, 3)) +B_packed = B_orig.reshape(m, env.BLOCK_OUT, n, env.BLOCK_IN).transpose((0, 2, 1, 3)) + +# 用 tvm.nd.array 将输入/输出数组格式化为 DLPack 标准 +A_nd = tvm.nd.array(A_packed, ctx) +B_nd = tvm.nd.array(B_packed, ctx) +C_nd = tvm.nd.array(np.zeros((o, m, env.BATCH, env.BLOCK_OUT)).astype(C.dtype), ctx) + +# 清除统计 +if env.TARGET in ["sim", "tsim"]: + simulator.clear_stats() + +# 调用模块进行计算 +f(A_nd, B_nd, C_nd) +``` + +## 验证正确性 + +用 numpy 计算推理结果,得出结论:矩阵乘法的输出是正确的。 + +``` python +# 用 numpy 计算参考结果 +C_ref = np.dot(A_orig.astype(env.acc_dtype), B_orig.T.astype(env.acc_dtype)).astype(C.dtype) +C_ref = C_ref.reshape(o, env.BATCH, m, env.BLOCK_OUT).transpose((0, 2, 1, 3)) +np.testing.assert_equal(C_ref, C_nd.numpy()) + +# 打印 stats +if env.TARGET in ["sim", "tsim"]: + sim_stats = simulator.stats() + print("Execution statistics:") + for k, v in sim_stats.items(): + print("\t{:<16}: {:>16}".format(k, v)) + +print("Successful matrix multiply test!") +``` + +输出结果: + +``` bash +Execution statistics: + inp_load_nbytes : 256 + wgt_load_nbytes : 65536 + acc_load_nbytes : 0 + uop_load_nbytes : 8 + out_store_nbytes: 256 + gemm_counter : 256 + alu_counter : 0 +Successful matrix multiply test! +``` + +## 总结 + +本教程展示了在 VTA 上实现简单矩阵乘法示例的 TVM 工作流程。一般工作流程包括: + +* 通过 RPC 使用 VTA 比特流对 FPGA 进行编程。 +* 通过一系列计算描述矩阵乘法。 +* 描述希望如何用调度原语执行计算。 +* 将函数编译为 VTA target。 +* 运行编译好的模块,并根据 numpy 实现进行验证。 + +[下载 Python 源代码:matrix_multiply.py](https://tvm.apache.org/docs/_downloads/de1c160863e8a3826753e987a4138298/matrix_multiply.py) + +[下载 Jupyter Notebook:matrix_multiply.ipynb](https://tvm.apache.org/docs/_downloads/1ee0b869c5082223c5dfbb0fe4574252/matrix_multiply.ipynb) diff --git a/versioned_docs/version-0.12.0/topic/vta/tutorials/02-start_vta.md b/versioned_docs/version-0.12.0/topic/vta/tutorials/02-start_vta.md new file mode 100644 index 00000000..9958a3b0 --- /dev/null +++ b/versioned_docs/version-0.12.0/topic/vta/tutorials/02-start_vta.md @@ -0,0 +1,392 @@ +--- +title: VTA 入门 +--- + +# VTA 入门 + +:::note +单击 [此处](https://tvm.apache.org/docs/topic/vta/tutorials/vta_get_started.html#sphx-glr-download-topic-vta-tutorials-vta-get-started-py) 下载完整的示例代码 +::: + +**作者**:[Thierry Moreau](https://homes.cs.washington.edu/\~moreau/) + +本文介绍如何用 TVM 对 VTA 设计进行编程。 + +本教程演示在 VTA 设计的向量 ALU 上,实现向量加法的基本 TVM 工作流程。此过程包括,将计算降级到底层加速器操作所需的特定 schedule 转换。 + +首先导入 TVM(深度学习优化编译器)。进行 VTA 设计,还需要导入 VTA Python 包,这个包里包含针对 TVM 的 VTA 特定扩展。 + +``` python +from __future__ import absolute_import, print_function + +import os +import tvm +from tvm import te +import vta +import numpy as np +``` + +## 加载 VTA 参数 + +VTA 的设计遵循模块化和可定制的原则。因此,用户可以自由修改影响硬件设计布局的高级硬件参数。这些参数在 `vta_config.json` 文件中由它们的 `log2` 值指定。这些 VTA 参数可以通过 `vta.get_env` 函数加载。 + +最后,在 `vta_config.json` 文件中指定 TVM target。当设置为 *sim* 时,会在行为 VTA 模拟器内执行。若要在 Pynq FPGA 开发平台上运行本教程,请遵循 *VTA 基于 Pynq 的测试设置指南*。 + +``` python +env = vta.get_env() +``` + +### FPGA 编程 + +以 Pynq FPGA 开发板为 target 时,要为开发板配置 VTA 比特流。 + +``` python +# 要 TVM RPC 模块和 VTA 模拟器模块 +from tvm import rpc +from tvm.contrib import utils +from vta.testing import simulator + +# 从 OS 环境中读取 Pynq RPC 主机 IP 地址和端口号 +host = os.environ.get("VTA_RPC_HOST", "192.168.2.99") +port = int(os.environ.get("VTA_RPC_PORT", "9091")) + +# 在 Pynq 上配置比特流和 runtime 系统 +# 匹配 vta_config.json 文件指定的 VTA 配置。 +if env.TARGET == "pynq" or env.TARGET == "de10nano": + + # 确保 TVM 是使用 RPC=1 编译的 + assert tvm.runtime.enabled("rpc") + remote = rpc.connect(host, port) + + # 重新配置 JIT runtime + vta.reconfig_runtime(remote) + + # 使用预编译的 VTA 比特流对 FPGA 进行编程。 + # 可以通过传递比特流文件的路径而非 None, + # 使用自定义比特流对 FPGA 进行编程 + vta.program_fpga(remote, bitstream=None) + +# 在模拟模式下,本地托管 RPC 服务器。 +elif env.TARGET in ("sim", "tsim", "intelfocl"): + remote = rpc.LocalSession() + + if env.TARGET in ["intelfocl"]: + # 对 intelfocl aocx 编程 + vta.program_fpga(remote, bitstream="vta.bitstream") +``` + +### 计算声明 + +第一步,描述计算。 TVM 采用张量语义,每个中间结果表示为多维数组。用户需要描述生成输出张量的计算规则。 + +此示例描述了一个向量加法,分为多个计算阶段,如下面的数据流图所示。首先,描述主存储器中的输入张量 `A` 和 `B`。然后,声明 VTA 芯片缓冲区里的中间张量 `A_buf` 和 `B_buf`。这个额外的计算阶段使得可以显式地暂存缓存的读和写。第三,描述将 `A_buf` 添加到 `B_buf`,产生 `C_buf` 的向量加法计算。最后一个操作是,强制转换并复制回 DRAM,产生结果张量 `C`。 + +![图片](/img/docs/uwsampl/web-data/main/vta/tutorial/vadd_dataflow.png) + +## 输入占位符 + +以平铺数据格式描述占位符张量 `A` 和 `B`,匹配 VTA 向量 ALU 规范的数据布局要求。 + +对于 VTA 的通用操作(例如向量相加),图块大小为 `(env.BATCH, env.BLOCK_OUT)`。维度在 `vta_config.json` 配置文件中指定,默认设置为 (1, 16) 向量。 + +此外,A 和 B 的数据类型还要和 `vta_config.json` 文件中设置的 `env.acc_dtype` 匹配,即为 32 位整数。 + +``` python +# 输出通道因子 m - 总共 64 x 16 = 1024 输出通道 +m = 64 +# Batch 因子 o - 总共 1 x 1 = 1 +o = 1 +# A 平铺数据格式的占位符张量 +A = te.placeholder((o, m, env.BATCH, env.BLOCK_OUT), name="A", dtype=env.acc_dtype) +# B 平铺数据格式的占位符张量 +B = te.placeholder((o, m, env.BATCH, env.BLOCK_OUT), name="B", dtype=env.acc_dtype) +``` + +## 拷贝缓冲区 + +硬件加速器的特点之一是必须显式管理芯片存储器。这意味着要描述中间张量 `A_buf` 和 `B_buf`,它们可以具有不同于原始占位符张量 `A` 和 `B` 的内存范围。 + +然后在调度阶段,告诉编译器 `A_buf` 和 `B_buf` 位于 VTA 的芯片缓冲区(SRAM)中,而 `A` 和 `B` 在主存储器(DRAM)中。将 `A_buf` 和 `B_buf` 描述为计算操作(恒等函数)的结果。编译器之后可将其解释为缓存的读操作。 + +``` python +# A 复制缓冲区 +A_buf = te.compute((o, m, env.BATCH, env.BLOCK_OUT), lambda *i: A(*i), "A_buf") +# B 复制缓冲区 +B_buf = te.compute((o, m, env.BATCH, env.BLOCK_OUT), lambda *i: B(*i), "B_buf") +``` + +## 向量加法 + +接下来用另一个计算操作来描述向量加法结果张量 `C`。计算函数接收张量的 shape,以及描述张量每个位置计算规则的 lambda 函数。 + +这个阶段只声明如何计算,不会发生任何计算。 + +``` python +# 描述 in-VTA 向量加法 +C_buf = te.compute( + (o, m, env.BATCH, env.BLOCK_OUT), + lambda *i: A_buf(*i).astype(env.acc_dtype) + B_buf(*i).astype(env.acc_dtype), + name="C_buf", +) +``` + +## 转换结果 + +计算完成后,将 VTA 计算的结果返回主存。 + +:::note +**内存存储限制** + +VTA 的特点之一是它只支持窄的 `env.inp_dtype` 数据类型格式的 DRAM 存储。这减少了内存传输的数据占用时间(在基本矩阵乘法示例中,对此进行了更多说明)。 +::: + +对窄的输入激活数据格式执行最后一个类型转换操作。 + +``` python +# 转换为输出类型,并发送到主存 +C = te.compute( + (o, m, env.BATCH, env.BLOCK_OUT), lambda *i: C_buf(*i).astype(env.inp_dtype), name="C" +) +``` + +本教程的计算声明部分到此结束。 + +### 调度计算 + +虽然上面描述了计算规则,但可以通过多种方式获得 `C`。TVM 要求用户提供名为 *schedule* 的计算实现。 + +schedule 是对原始计算的一组转换,它在不影响正确性的情况下,转换计算的实现。这个简单的 VTA 编程教程旨在演示将原始 schedule 映射到 VTA 硬件原语的基本 schedule 转换。 + +## 默认 Schedule + +构建 schedule 后,schedule 默认以下面的方式计算 `C`: + +``` python +# 查看生成的 schedule +s = te.create_schedule(C.op) + +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(int32), int32, [1024], []), + B: Buffer(B_2: Pointer(int32), int32, [1024], []), + C: Buffer(C_2: Pointer(int8), int8, [1024], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, int32, [1, 64, 1, 16], []), B_1: B_3: Buffer(B_2, int32, [1, 64, 1, 16], []), C_1: C_3: Buffer(C_2, int8, [1, 64, 1, 16], [])} { + allocate(A_buf: Pointer(global int32), int32, [1024]), storage_scope = global; + allocate(B_buf: Pointer(global int32), int32, [1024]), storage_scope = global { + for (i1: int32, 0, 64) { + for (i3: int32, 0, 16) { + let cse_var_1: int32 = ((i1*16) + i3) + A_buf_1: Buffer(A_buf, int32, [1024], [])[cse_var_1] = A[cse_var_1] + } + } + for (i1_1: int32, 0, 64) { + for (i3_1: int32, 0, 16) { + let cse_var_2: int32 = ((i1_1*16) + i3_1) + B_buf_1: Buffer(B_buf, int32, [1024], [])[cse_var_2] = B[cse_var_2] + } + } + for (i1_2: int32, 0, 64) { + for (i3_2: int32, 0, 16) { + let cse_var_3: int32 = ((i1_2*16) + i3_2) + A_buf_2: Buffer(A_buf, int32, [1024], [])[cse_var_3] = (A_buf_1[cse_var_3] + B_buf_1[cse_var_3]) + } + } + for (i1_3: int32, 0, 64) { + for (i3_3: int32, 0, 16) { + let cse_var_4: int32 = ((i1_3*16) + i3_3) + C[cse_var_4] = cast(int8, A_buf_2[cse_var_4]) + } + } + } +} +``` + +虽然这个 schedule 有意义,但它不会编译为 VTA。为了生成正确的代码,要应用调度原语和代码注释,将调度转换为可以直接降级到 VTA 硬件内联函数的调度。包括: + +* DMA 复制操作,接收全局范围的张量,并将其复制到局部范围的张量中。 +* 即将执行向量加法的向量 ALU 操作。 + +## 缓冲区范围 + +首先,设置拷贝缓冲区的范围,告诉 TVM 这些中间张量存储在 VTA 的芯片 SRAM 缓冲区中。接下来告诉 TVM,`A_buf`、`B_buf`、`C_buf` 在 VTA 芯片*累加器缓冲区*中,该缓冲区用作 VTA 的通用寄存器文件。 + +将中间张量的范围设置为 VTA 的芯片累加器缓冲区。 + +``` python +s[A_buf].set_scope(env.acc_scope) +s[B_buf].set_scope(env.acc_scope) +s[C_buf].set_scope(env.acc_scope) +``` + +输出结果: + +``` bash +stage(C_buf, compute(C_buf, body=[(A_buf[i0, i1, i2, i3] + B_buf[i0, i1, i2, i3])], axis=[iter_var(i0, range(min=0, ext=1)), iter_var(i1, range(min=0, ext=64)), iter_var(i2, range(min=0, ext=1)), iter_var(i3, range(min=0, ext=16))], reduce_axis=[], tag=, attrs={})) +``` + +## DMA 传输 + +调度 DMA 传输,将 DRAM 中的数据移入和移出 VTA 芯片缓冲区。插入 `dma_copy` 编译指示,向编译器表明复制操作将通过 DMA 批量执行,这在硬件加速器中很常见。 + +``` python +# 用 DMA 编译指示标记缓冲区拷贝,将拷贝循环映射到 +# DMA 传输操作 +s[A_buf].pragma(s[A_buf].op.axis[0], env.dma_copy) +s[B_buf].pragma(s[B_buf].op.axis[0], env.dma_copy) +s[C].pragma(s[C].op.axis[0], env.dma_copy) +``` + +## ALU 操作 + +VTA 有一个向量 ALU,可以对累加器缓冲区中的张量执行向量运算。若要告诉 TVM 给定操作要映射到 VTA 的向量 ALU,需要用 `env.alu` 编译指示来显式标记向量加法循环。 + +``` python +# 告诉 TVM 要在 VTA 的向量 ALU 上执行计算 +s[C_buf].pragma(C_buf.op.axis[0], env.alu) + +# 查看最终确定的 schedule +print(vta.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(int32), int32, [1024], []), + B: Buffer(B_2: Pointer(int32), int32, [1024], []), + C: Buffer(C_2: Pointer(int8), int8, [1024], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, int32, [1, 64, 1, 16], []), B_1: B_3: Buffer(B_2, int32, [1, 64, 1, 16], []), C_1: C_3: Buffer(C_2, int8, [1, 64, 1, 16], [])} { + attr [IterVar(vta: int32, (nullptr), "ThreadIndex", "vta")] "coproc_scope" = 2 { + @tir.call_extern("VTALoadBuffer2D", @tir.tvm_thread_context(@tir.vta.command_handle(, dtype=handle), dtype=handle), A_2, 0, 64, 1, 64, 0, 0, 0, 0, 0, 3, dtype=int32) + @tir.call_extern("VTALoadBuffer2D", @tir.tvm_thread_context(@tir.vta.command_handle(, dtype=handle), dtype=handle), B_2, 0, 64, 1, 64, 0, 0, 0, 0, 64, 3, dtype=int32) + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_uop_scope" = "VTAPushALUOp" { + @tir.call_extern("VTAUopLoopBegin", 64, 1, 1, 0, dtype=int32) + @tir.vta.uop_push(1, 0, 0, 64, 0, 2, 0, 0, dtype=int32) + @tir.call_extern("VTAUopLoopEnd", dtype=int32) + } + @tir.vta.coproc_dep_push(2, 3, dtype=int32) + } + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_scope" = 3 { + @tir.vta.coproc_dep_pop(2, 3, dtype=int32) + @tir.call_extern("VTAStoreBuffer2D", @tir.tvm_thread_context(@tir.vta.command_handle(, dtype=handle), dtype=handle), 0, 4, C_2, 0, 64, 1, 64, dtype=int32) + } + @tir.vta.coproc_sync(, dtype=int32) +} +``` + +本教程的调度部分到此结束。 + +### TVM 编译 + +指定 schedule 后,可以将其编译为 TVM 函数。TVM 默认编译为一个类型擦除的函数,可以直接从 Python 端调用。 + +以下代码用 `tvm.build` 创建一个函数。build 函数接收 schedule、所需的函数签名(包括输入和输出)以及要编译到的目标语言。 + +``` python +my_vadd = vta.build( + s, [A, B, C], tvm.target.Target("ext_dev", host=env.target_host), name="my_vadd" +) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:267: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +## 保存模块 + +TVM 将模块保存到文件中,方便后续加载。这称为 ahead-of-time 编译,可以节省一些编译时间。更重要的是,这使得我们可以在开发机器上交叉编译可执行文件,并通过 RPC 将其发送到 Pynq FPGA 板执行。 + +``` python +# 将编译后的模块写入目标文件。 +temp = utils.tempdir() +my_vadd.save(temp.relpath("vadd.o")) + +# 通过 RPC 发送可执行文件 +remote.upload(temp.relpath("vadd.o")) +``` + +## 加载模块 + +可以从文件系统中加载编译好的模块来运行代码。 + +``` python +f = remote.load_module("vadd.o") +``` + +### 运行函数 + +编译好的 TVM 函数使用简洁的 C API,它可从任意语言调用。 + +TVM 在 Python 中提供了一个基于 [DLPack](https://github.com/dmlc/dlpack) 标准的数组 API,来快速测试和原型设计。 + +* 首先创建一个远程 context(用于在 Pynq 上远程执行)。 +* 然后 `tvm.nd.array` 相应地格式化数据。 +* `f()` 运行实际计算。 +* `numpy()` 以可解释的格式拷贝结果数组。 + +``` python +# 获取远程设备 context +ctx = remote.ext_dev(0) + +# 在(-128, 128] 的 int 范围内随机初始化 A 和 B 数组 +A_orig = np.random.randint(-128, 128, size=(o * env.BATCH, m * env.BLOCK_OUT)).astype(A.dtype) +B_orig = np.random.randint(-128, 128, size=(o * env.BATCH, m * env.BLOCK_OUT)).astype(B.dtype) + +# 将 A 和 B 数组从 2D 打包为 4D 打包布局 +A_packed = A_orig.reshape(o, env.BATCH, m, env.BLOCK_OUT).transpose((0, 2, 1, 3)) +B_packed = B_orig.reshape(o, env.BATCH, m, env.BLOCK_OUT).transpose((0, 2, 1, 3)) + +# 用 tvm.nd.array 将输入/输出数组格式化为 DLPack 标准 +A_nd = tvm.nd.array(A_packed, ctx) +B_nd = tvm.nd.array(B_packed, ctx) +C_nd = tvm.nd.array(np.zeros((o, m, env.BATCH, env.BLOCK_OUT)).astype(C.dtype), ctx) + +# 调用模块进行计算 +f(A_nd, B_nd, C_nd) +``` + +### 验证正确性 + +用 numpy 计算推理结果,并得出结论:矩阵乘法的输出是正确的。 + +``` python +# 用 numpy 计算参考结果 +C_ref = (A_orig.astype(env.acc_dtype) + B_orig.astype(env.acc_dtype)).astype(C.dtype) +C_ref = C_ref.reshape(o, env.BATCH, m, env.BLOCK_OUT).transpose((0, 2, 1, 3)) +np.testing.assert_equal(C_ref, C_nd.numpy()) +print("Successful vector add test!") +``` + +输出结果: + +``` bash +Successful vector add test! +``` + +### 总结 + +本教程通过一个简单的向量加法示例,展示了如何用 TVM 对深度学习加速器 VTA 进行编程。一般工作流程包括: + +* 通过 RPC 用 VTA 比特流对 FPGA 进行编程。 +* 通过一系列计算来描述向量加法计算。 +* 描述如何用调度原语执行计算。 +* 将函数编译为 VTA target。 +* 运行编译好的模块,并根据 numpy 实现进行验证。 + +欢迎查看其他示例和教程,了解有关 TVM 支持的操作、调度原语和其他功能的更多信息,从而对 VTA 进行编程。 + +[下载 Python 源代码:vta_get_started.py](https://tvm.apache.org/docs/_downloads/d2434fbd36b5bd5a93a69ca80465d5b6/vta_get_started.py) + +[下载 Jupyter Notebook:vta_get_started.ipynb](https://tvm.apache.org/docs/_downloads/83b9961c758069912464db3443fffc06/vta_get_started.ipynb) diff --git a/versioned_docs/version-0.12.0/topic/vta/tutorials/03-deploy_mxnet.md b/versioned_docs/version-0.12.0/topic/vta/tutorials/03-deploy_mxnet.md new file mode 100644 index 00000000..e40bcc7b --- /dev/null +++ b/versioned_docs/version-0.12.0/topic/vta/tutorials/03-deploy_mxnet.md @@ -0,0 +1,320 @@ +--- +title: 在 VTA 上部署来自 MxNet 的预训练视觉模型 +--- + +# 在 VTA 上部署来自 MxNet 的预训练视觉模型 + +:::note +单击 [此处](https://tvm.apache.org/docs/topic/vta/tutorials/frontend/deploy_classification.html#sphx-glr-download-topic-vta-tutorials-frontend-deploy-classification-py) 下载完整的示例代码 +::: + +**作者**:[Thierry Moreau](https://homes.cs.washington.edu/\~moreau/) + +本教程提供了一个端到端 demo,介绍了如何在 VTA 加速器设计上运行 ImageNet 分类推理,执行 ImageNet 分类任务。它展示了 Relay 作为一个前端编译器,可以执行量化(VTA 仅支持 int8/32 推理)以及计算图打包(为了在 core 中启用张量),从而为硬件 target 修改计算图。 + +## 安装依赖 + +要在 TVM 中使用 autotvm 包,需要安装额外的依赖(如果用的是 Python2,请将「3」更改为「2」): + +``` bash +pip3 install --user mxnet requests "Pillow<7" +``` + +在 Python 代码中导入包: + +``` python +from __future__ import absolute_import, print_function + +import argparse, json, os, requests, sys, time +from io import BytesIO +from os.path import join, isfile +from PIL import Image + +from mxnet.gluon.model_zoo import vision +import numpy as np +from matplotlib import pyplot as plt + +import tvm +from tvm import te +from tvm import rpc, autotvm, relay +from tvm.contrib import graph_executor, utils, download +from tvm.contrib.debugger import debug_executor +from tvm.relay import transform + +import vta +from vta.testing import simulator +from vta.top import graph_pack + +# 确保 TVM 是使用 RPC=1 编译的 +assert tvm.runtime.enabled("rpc") +``` + +## 定义平台和模型 targets + +对比在 CPU 与 VTA 上执行,并定义模型。 + +``` python +# 从 3rdparty/vta-hw/config/vta_config.json 文件加载 VTA 参数 +env = vta.get_env() + +# 设置 ``device=arm_cpu`` 在 CPU 上运行推理 +# 设置 ``device=vta`` 在 FPGA 上运行推理 +device = "vta" +target = env.target if device == "vta" else env.target_vta_cpu + +# 查找何时开始/结束位打包的字典 +pack_dict = { + "resnet18_v1": ["nn.max_pool2d", "nn.global_avg_pool2d"], + "resnet34_v1": ["nn.max_pool2d", "nn.global_avg_pool2d"], + "resnet18_v2": ["nn.max_pool2d", "nn.global_avg_pool2d"], + "resnet34_v2": ["nn.max_pool2d", "nn.global_avg_pool2d"], + "resnet50_v2": ["nn.max_pool2d", "nn.global_avg_pool2d"], + "resnet101_v2": ["nn.max_pool2d", "nn.global_avg_pool2d"], +} + +# 要编译的 Gluon 模型的名称 +# ``start_pack`` 和 ``stop_pack`` 标签指示在哪里 +# 开始和结束计算图打包 Relay pass:换言之, +# 从哪里开始和完成转移到 VTA。 +model = "resnet18_v1" +assert model in pack_dict +``` + +## 获取远程执行 + +当 target 是「pynq」时,重新配置 FPGA 和 runtime。若 target 是「sim」,则在本地执行。 + +``` python +if env.TARGET not in ["sim", "tsim", "intelfocl"]: + # 若设置了环境变量,则从跟踪器节点获取远程。 + # 要设置跟踪器,参考「自动调优 VTA 的卷积网络」教程。 + tracker_host = os.environ.get("TVM_TRACKER_HOST", None) + tracker_port = os.environ.get("TVM_TRACKER_PORT", None) + # 否则,若有一个想直接从主机编程的设备, + # 请确保已将以下变量设置为你板子的 IP + device_host = os.environ.get("VTA_RPC_HOST", "192.168.2.99") + device_port = os.environ.get("VTA_RPC_PORT", "9091") + if not tracker_host or not tracker_port: + remote = rpc.connect(device_host, int(device_port)) + else: + remote = autotvm.measure.request_remote( + env.TARGET, tracker_host, int(tracker_port), timeout=10000 + ) + + # 重新配置 JIT runtime 和 FPGA。 + # 可以通过传递比特流文件的路径而非 None,用自定义比特流对 FPGA 进行编程 + reconfig_start = time.time() + vta.reconfig_runtime(remote) + vta.program_fpga(remote, bitstream=None) + reconfig_time = time.time() - reconfig_start + print("Reconfigured FPGA and RPC runtime in {0:.2f}s!".format(reconfig_time)) + +# 在模拟模式下,本地托管 RPC 服务器。 +else: + remote = rpc.LocalSession() + + if env.TARGET in ["intelfocl"]: + # 编写 intelfocl aocx + vta.program_fpga(remote, bitstream="vta.bitstream") + +# 从远程获取执行上下文 +ctx = remote.ext_dev(0) if device == "vta" else remote.cpu(0) +``` + +## 构建推理图执行器 + +从 Gluon model zoo 选取视觉模型,并用 Relay 编译。编译步骤如下: + +1. 从 MxNet 到 Relay 模块的前端转换。 +2. 应用 8 位量化:这里跳过第一个 conv 层和 dense 层,它们都将在 CPU 上以 fp32 执行。 +3. 执行计算图打包,更改张量化的数据布局。 +4. 执行常量折叠,减少算子的数量(例如,消除 batch norm multiply)。 +5. 对目标文件执行 Relay 构建。 +6. 将目标文件加载到远程(FPGA 设备)。 +7. 生成图执行器 *m*。 + +``` python +# 加载预先配置的 AutoTVM schedules +with autotvm.tophub.context(target): + # 为 ImageNet 分类器输入填充 shape 和数据类型字典 + dtype_dict = {"data": "float32"} + shape_dict = {"data": (env.BATCH, 3, 224, 224)} + + # 下架 gluon 模型,并转换为 Relay + gluon_model = vision.get_model(model, pretrained=True) + + # 测试构建开始时间 + build_start = time.time() + + # 开始前端编译 + mod, params = relay.frontend.from_mxnet(gluon_model, shape_dict) + + # 更新 shape 和类型字典 + shape_dict.update({k: v.shape for k, v in params.items()}) + dtype_dict.update({k: str(v.dtype) for k, v in params.items()}) + + if target.device_name == "vta": + # 在 Relay 中执行量化 + # 注意:将 opt_level 设置为 3 ,折叠 batch norm + with tvm.transform.PassContext(opt_level=3): + with relay.quantize.qconfig(global_scale=8.0, skip_conv_layers=[0]): + mod = relay.quantize.quantize(mod, params=params) + # 对 VTA 目标进行图打包和常量折叠 + assert env.BLOCK_IN == env.BLOCK_OUT + # 若 target 是 intelfocl 或 sim,则进行设备注释 + relay_prog = graph_pack( + mod["main"], + env.BATCH, + env.BLOCK_OUT, + env.WGT_WIDTH, + start_name=pack_dict[model][0], + stop_name=pack_dict[model][1], + device_annot=(env.TARGET == "intelfocl"), + ) + else: + relay_prog = mod["main"] + + # 在禁用 AlterOpLayout 的情况下,编译 Relay 程序 + if target.device_name != "vta": + with tvm.transform.PassContext(opt_level=3, disabled_pass={"AlterOpLayout"}): + graph, lib, params = relay.build( + relay_prog, target=tvm.target.Target(target, host=env.target_host), params=params + ) + else: + if env.TARGET == "intelfocl": + # 在 cpu 和 vta 上运行多个 target + target = {"cpu": env.target_vta_cpu, "ext_dev": target} + with vta.build_config( + opt_level=3, disabled_pass={"AlterOpLayout", "tir.CommonSubexprElimTIR"} + ): + graph, lib, params = relay.build( + relay_prog, target=tvm.target.Target(target, host=env.target_host), params=params + ) + + # 测试 Relay 构建时间 + build_time = time.time() - build_start + print(model + " inference graph built in {0:.2f}s!".format(build_time)) + + # 将推理库发送到远程 RPC 服务器 + temp = utils.tempdir() + lib.export_library(temp.relpath("graphlib.tar")) + remote.upload(temp.relpath("graphlib.tar")) + lib = remote.load_module("graphlib.tar") + + if env.TARGET == "intelfocl": + ctxes = [remote.ext_dev(0), remote.cpu(0)] + m = graph_executor.create(graph, lib, ctxes) + else: + # 计算图 runtime + m = graph_executor.create(graph, lib, ctx) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:267: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +/workspace/python/tvm/relay/build_module.py:411: DeprecationWarning: Please use input parameter mod (tvm.IRModule) instead of deprecated parameter mod (tvm.relay.function.Function) + DeprecationWarning, +/workspace/vta/tutorials/frontend/deploy_classification.py:213: DeprecationWarning: legacy graph executor behavior of producing json / lib / params will be removed in the next release. Please see documents of tvm.contrib.graph_executor.GraphModule for the new recommended usage. + relay_prog, target=tvm.target.Target(target, host=env.target_host), params=params +resnet18_v1 inference graph built in 22.98s! +``` + +## 执行图像分类推理 + +对来自 ImageNet 的图像样本进行分类。只需下载类别文件、*synset.txt* 和输入测试图像。 + +``` python +# 下载 ImageNet 类别 +categ_url = "https://github.com/uwsampl/web-data/raw/main/vta/models/" +categ_fn = "synset.txt" +download.download(join(categ_url, categ_fn), categ_fn) +synset = eval(open(categ_fn).read()) + +# 下载测试图像 +image_url = "https://homes.cs.washington.edu/~moreau/media/vta/cat.jpg" +image_fn = "cat.png" +download.download(image_url, image_fn) + +# 为推理准备测试图像 +image = Image.open(image_fn).resize((224, 224)) +plt.imshow(image) +plt.show() +image = np.array(image) - np.array([123.0, 117.0, 104.0]) +image /= np.array([58.395, 57.12, 57.375]) +image = image.transpose((2, 0, 1)) +image = image[np.newaxis, :] +image = np.repeat(image, env.BATCH, axis=0) + +# 设置网络参数和输入 +m.set_input(**params) +m.set_input("data", image) + +# 执行推理,并收集执行统计信息 +# 更多信息::py:method:`tvm.runtime.Module.time_evaluator` +num = 4 # 为单个测试运行模块的次数 +rep = 3 # 测试次数(我们从中得出标准差) +timer = m.module.time_evaluator("run", ctx, number=num, repeat=rep) + +if env.TARGET in ["sim", "tsim"]: + simulator.clear_stats() + timer() + sim_stats = simulator.stats() + print("\nExecution statistics:") + for k, v in sim_stats.items(): + # 由于多次执行工作流程,需要对统计数据归一化 + # 注意,总有一次预运行 + # 因此将整体统计数据除以 (num * rep + 1) + print("\t{:<16}: {:>16}".format(k, v // (num * rep + 1))) +else: + tcost = timer() + std = np.std(tcost.results) * 1000 + mean = tcost.mean * 1000 + print("\nPerformed inference in %.2fms (std = %.2f) for %d samples" % (mean, std, env.BATCH)) + print("Average per sample inference time: %.2fms" % (mean / env.BATCH)) + +# 获取分类结果 +tvm_output = m.get_output(0, tvm.nd.empty((env.BATCH, 1000), "float32", remote.cpu(0))) +for b in range(env.BATCH): + top_categories = np.argsort(tvm_output.numpy()[b]) + # 报告前 5 个分类结果 + print("\n{} prediction for sample {}".format(model, b)) + print("\t#1:", synset[top_categories[-1]]) + print("\t#2:", synset[top_categories[-2]]) + print("\t#3:", synset[top_categories[-3]]) + print("\t#4:", synset[top_categories[-4]]) + print("\t#5:", synset[top_categories[-5]]) + # 这只检查前 5 个类别之一是一种猫;这绝不是有关量化如何影响分类准确性的准确评估,而是在捕捉对 CI 准确性的量化 pass 的变化。 + cat_detected = False + for k in top_categories[-5:]: + if "cat" in synset[k]: + cat_detected = True + assert cat_detected +``` + +![deploy classification](https://tvm.apache.org/docs/_images/sphx_glr_deploy_classification_001.png) + +输出结果: + +``` bash +Execution statistics: + inp_load_nbytes : 5549568 + wgt_load_nbytes : 12763136 + acc_load_nbytes : 6051840 + uop_load_nbytes : 22864 + out_store_nbytes: 2433536 + gemm_counter : 6623232 + alu_counter : 699328 + +resnet18_v1 prediction for sample 0 + #1: tiger cat + #2: Egyptian cat + #3: tabby, tabby cat + #4: lynx, catamount + #5: weasel +``` + +[下载 Python 源代码:deploy_classification.py](https://tvm.apache.org/docs/_downloads/9e8de33a5822b31748bfd76861009f92/deploy_classification.py) + +[下载 Jupyter Notebook:deploy_classification.ipynb](https://tvm.apache.org/docs/_downloads/95395e118195f25266654dd8fbf487d4/deploy_classification.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/topic/vta/tutorials/04-deploy_darknet.md b/versioned_docs/version-0.12.0/topic/vta/tutorials/04-deploy_darknet.md new file mode 100644 index 00000000..02290f9b --- /dev/null +++ b/versioned_docs/version-0.12.0/topic/vta/tutorials/04-deploy_darknet.md @@ -0,0 +1,326 @@ +--- +title: 在 VTA 上部署来自 Darknet 的预训练视觉检测模型 +--- + +# 在 VTA 上部署来自 Darknet 的预训练视觉检测模型 + +:::note +单击 [此处](https://tvm.apache.org/docs/topic/vta/tutorials/frontend/deploy_detection.html#sphx-glr-download-topic-vta-tutorials-frontend-deploy-detection-py) 下载完整的示例代码 +::: + +**作者**:[Hua Jiang](https://github.com/huajsj) + +本教程提供了一个端到端 demo,介绍了如何在 VTA 加速器设计上运行 Darknet YoloV3-tiny 推理,执行图像检测任务。它展示了 Relay 作为一个前端编译器,可以执行量化(VTA 仅支持 int8/32 推理)以及计算图打包(为了在 core 中启用张量),从而为硬件 target 修改计算图。 + +## 安装依赖 + +要在 TVM 中使用 autotvm 包,需要安装额外的依赖(如果用的是 Python2,请将「3」更改为「2」): + +``` bash +pip3 install "Pillow<7" +``` + +支持 Darknet 解析的 YOLO-V3-tiny 模型依赖于 CFFI 和 CV2 库,因此要在执行此脚本之前安装 CFFI 和 CV2。 + +``` bash +pip3 install cffi +pip3 install opencv-python +``` + +在 Python 代码中导入包: + +``` python +from __future__ import absolute_import, print_function + +import sys +import os +import time +import matplotlib.pyplot as plt +import numpy as np +import tvm +import vta +from tvm import rpc, autotvm, relay +from tvm.relay.testing import yolo_detection, darknet +from tvm.relay.testing.darknet import __darknetffi__ +from tvm.contrib import graph_executor, utils +from tvm.contrib.download import download_testdata +from vta.testing import simulator +from vta.top import graph_pack + +# 确保 TVM 是使用 RPC=1 编译的 +assert tvm.runtime.enabled("rpc") +``` + +根据 Model Name 下载 yolo net 配置文件、权重文件、Darknet 库文件 +———————————————————————————- + +``` python +MODEL_NAME = "yolov3-tiny" +REPO_URL = "https://github.com/dmlc/web-data/blob/main/darknet/" + +cfg_path = download_testdata( + "https://github.com/pjreddie/darknet/blob/master/cfg/" + MODEL_NAME + ".cfg" + "?raw=true", + MODEL_NAME + ".cfg", + module="darknet", +) +weights_path = download_testdata( + "https://pjreddie.com/media/files/" + MODEL_NAME + ".weights" + "?raw=true", + MODEL_NAME + ".weights", + module="darknet", +) + +if sys.platform in ["linux", "linux2"]: + darknet_lib_path = download_testdata( + REPO_URL + "lib/" + "libdarknet2.0.so" + "?raw=true", "libdarknet2.0.so", module="darknet" + ) +elif sys.platform == "darwin": + darknet_lib_path = download_testdata( + REPO_URL + "lib_osx/" + "libdarknet_mac2.0.so" + "?raw=true", + "libdarknet_mac2.0.so", + module="darknet", + ) +else: + raise NotImplementedError("Darknet lib is not supported on {} platform".format(sys.platform)) +``` + +## 下载 YOLO 标签名称和对应插图 + +``` python +coco_path = download_testdata( + REPO_URL + "data/" + "coco.names" + "?raw=true", "coco.names", module="data" +) +font_path = download_testdata( + REPO_URL + "data/" + "arial.ttf" + "?raw=true", "arial.ttf", module="data" +) +with open(coco_path) as f: + content = f.readlines() +names = [x.strip() for x in content] +``` + +## 定义平台和模型 target + +对比在 CPU 与 VTA 上执行,并定义模型。 + +``` python +# 从 3rdparty/vta-hw/config/vta_config.json 文件加载 VTA 参数 +env = vta.get_env() +# 设置 ``device=arm_cpu``,在 CPU 上运行推理 +# 设置 ``device=vta`` 在 FPGA 上运行推理 +device = "vta" +target = env.target if device == "vta" else env.target_vta_cpu + +pack_dict = { + "yolov3-tiny": ["nn.max_pool2d", "cast", 4, 186], +} + +# 要编译的 Darknet 模型的名称 +# ``start_pack`` 和 ``stop_pack`` 标签指示在哪里开始和结束计算图打包 Relay pass:换句话说,从哪里开始和结束转移到 VTA。 +# 数字 4 表示 ``start_pack`` 索引是 4,数字 186 表示 ``stop_pack 索引`` 是 186,通过使用名称和索引号,可以在这里找到正确的开始/结束的位置是多个 ``nn.max_pool2d`` 或 ``cast``, print(mod.astext(show_meta_data=False)) 可以帮助查找算子名称和索引信息。 +assert MODEL_NAME in pack_dict +``` + +## 获取远程执行 + +当 target 是「pynq」或其他 FPGA 后端时,重新配置 FPGA 和 runtime。若 target 是「sim」,则在本地执行。 + +``` python +if env.TARGET not in ["sim", "tsim"]: + # 若设置了环境变量,则从跟踪器节点获取远程。 + # 要设置跟踪器,你需要遵循「为 VTA 自动调优卷积网络」教程。 + tracker_host = os.environ.get("TVM_TRACKER_HOST", None) + tracker_port = os.environ.get("TVM_TRACKER_PORT", None) + # 否则,若有一个想要直接从主机编程的设备,请确保已将以下变量设置为你的板子的 IP。 + device_host = os.environ.get("VTA_RPC_HOST", "192.168.2.99") + device_port = os.environ.get("VTA_RPC_PORT", "9091") + if not tracker_host or not tracker_port: + remote = rpc.connect(device_host, int(device_port)) + else: + remote = autotvm.measure.request_remote( + env.TARGET, tracker_host, int(tracker_port), timeout=10000 + ) + # 重新配置 JIT runtime 和 FPGA + # 可以通过将路径传递给比特流文件,而非 None,使用自定义比特流对 FPGA 进行编程。 + reconfig_start = time.time() + vta.reconfig_runtime(remote) + vta.program_fpga(remote, bitstream=None) + reconfig_time = time.time() - reconfig_start + print("Reconfigured FPGA and RPC runtime in {0:.2f}s!".format(reconfig_time)) + +# 在模拟模式下,本地托管 RPC 服务器。 +else: + remote = rpc.LocalSession() + +# 从远程获取执行上下文 +ctx = remote.ext_dev(0) if device == "vta" else remote.cpu(0) +``` + +## 构建推理图执行器 + +用 Darknet 库加载下载的视觉模型,并用 Relay 编译。编译步骤如下: + +1. 从 Darknet 到 Relay 模块的前端转换。 +2. 应用 8 位量化:这里跳过第一个 conv 层和 dense 层,它们都将在 CPU 上以 fp32 执行。 +3. 执行计算图打包,更改张量化的数据布局。 +4. 执行常量折叠,减少算子的数量(例如,消除 batch norm multiply)。 +5. 对目标文件执行 Relay 构建。 +6. 将目标文件加载到远程(FPGA 设备)。 +7. 生成图执行器 *m*。 + +``` python +# 加载预先配置的 AutoTVM schedule +with autotvm.tophub.context(target): + net = __darknetffi__.dlopen(darknet_lib_path).load_network( + cfg_path.encode("utf-8"), weights_path.encode("utf-8"), 0 + ) + dshape = (env.BATCH, net.c, net.h, net.w) + dtype = "float32" + + # 测试构建开始时间 + build_start = time.time() + + # 开始前端编译 + mod, params = relay.frontend.from_darknet(net, dtype=dtype, shape=dshape) + + if target.device_name == "vta": + # 在 Relay 中执行量化 + # 注意:将 opt_level 设置为 3,折叠 batch norm + with tvm.transform.PassContext(opt_level=3): + with relay.quantize.qconfig( + global_scale=23.0, + skip_conv_layers=[0], + store_lowbit_output=True, + round_for_shift=True, + ): + mod = relay.quantize.quantize(mod, params=params) + # 对 VTA target 进行计算图打包和常量折叠 + mod = graph_pack( + mod["main"], + env.BATCH, + env.BLOCK_OUT, + env.WGT_WIDTH, + start_name=pack_dict[MODEL_NAME][0], + stop_name=pack_dict[MODEL_NAME][1], + start_name_idx=pack_dict[MODEL_NAME][2], + stop_name_idx=pack_dict[MODEL_NAME][3], + ) + else: + mod = mod["main"] + + # 在禁用 AlterOpLayout 的情况下,编译 Relay 程序 + with vta.build_config(disabled_pass={"AlterOpLayout", "tir.CommonSubexprElimTIR"}): + lib = relay.build( + mod, target=tvm.target.Target(target, host=env.target_host), params=params + ) + + # 测试 Relay 构建时间 + build_time = time.time() - build_start + print(MODEL_NAME + " inference graph built in {0:.2f}s!".format(build_time)) + + # 将推理库发送到远程 RPC 服务器 + temp = utils.tempdir() + lib.export_library(temp.relpath("graphlib.tar")) + remote.upload(temp.relpath("graphlib.tar")) + lib = remote.load_module("graphlib.tar") + + # 图执行器 + m = graph_executor.GraphModule(lib["default"](ctx)) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:267: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +/workspace/python/tvm/relay/build_module.py:411: DeprecationWarning: Please use input parameter mod (tvm.IRModule) instead of deprecated parameter mod (tvm.relay.function.Function) + DeprecationWarning, +yolov3-tiny inference graph built in 16.12s! +``` + +## 执行图像检测推理 + +在下载的测试图像上运行检测 + +``` python +[neth, netw] = dshape[2:] +test_image = "person.jpg" +img_url = REPO_URL + "data/" + test_image + "?raw=true" +img_path = download_testdata(img_url, test_image, "data") +data = darknet.load_image(img_path, neth, netw).transpose(1, 2, 0) + +# 为推理准备测试图像 +plt.imshow(data) +plt.show() +data = data.transpose((2, 0, 1)) +data = data[np.newaxis, :] +data = np.repeat(data, env.BATCH, axis=0) + +# 设置网络参数和输入 +m.set_input("data", data) + +# 执行推理,并收集执行统计信息 +# 更多信息::py:method:`tvm.runtime.Module.time_evaluator` +num = 4 # 单次测试运行模块的次数 +rep = 3 # 测试次数(我们从中得出标准差) +timer = m.module.time_evaluator("run", ctx, number=num, repeat=rep) + +if env.TARGET in ["sim", "tsim"]: + simulator.clear_stats() + timer() + sim_stats = simulator.stats() + print("\nExecution statistics:") + for k, v in sim_stats.items(): + # 由于我们多次执行工作负载,我们需要标准化统计信息 + # 注意,总是有一个预热 + # 因此将整体统计数据除以 (num * rep + 1) + print("\t{:<16}: {:>16}".format(k, v // (num * rep + 1))) +else: + tcost = timer() + std = np.std(tcost.results) * 1000 + mean = tcost.mean * 1000 + print("\nPerformed inference in %.2fms (std = %.2f) for %d samples" % (mean, std, env.BATCH)) + print("Average per sample inference time: %.2fms" % (mean / env.BATCH)) + +# 从 out 获取检测结果 +thresh = 0.5 +nms_thresh = 0.45 +tvm_out = [] +for i in range(2): + layer_out = {} + layer_out["type"] = "Yolo" + # 获取 yolo 层的属性 (n、out_c、out_h、out_w、classes、total) + layer_attr = m.get_output(i * 4 + 3).numpy() + layer_out["biases"] = m.get_output(i * 4 + 2).numpy() + layer_out["mask"] = m.get_output(i * 4 + 1).numpy() + out_shape = (layer_attr[0], layer_attr[1] // layer_attr[0], layer_attr[2], layer_attr[3]) + layer_out["output"] = m.get_output(i * 4).numpy().reshape(out_shape) + layer_out["classes"] = layer_attr[4] + tvm_out.append(layer_out) + thresh = 0.560 + +# 显示检测结果 +img = darknet.load_image_color(img_path) +_, im_h, im_w = img.shape +dets = yolo_detection.fill_network_boxes((netw, neth), (im_w, im_h), thresh, 1, tvm_out) +last_layer = net.layers[net.n - 1] +yolo_detection.do_nms_sort(dets, last_layer.classes, nms_thresh) +yolo_detection.draw_detections(font_path, img, dets, thresh, names, last_layer.classes) +plt.imshow(img.transpose(1, 2, 0)) +plt.show() +``` + +![deploy detection](https://tvm.apache.org/docs/_images/sphx_glr_deploy_detection_001.png) + +``` bash +Execution statistics: + inp_load_nbytes : 25462784 + wgt_load_nbytes : 17558016 + acc_load_nbytes : 96128 + uop_load_nbytes : 5024 + out_store_nbytes: 3396224 + gemm_counter : 10578048 + alu_counter : 849056 +``` + +[下载 Python 源代码:deploy_detection.py](https://tvm.apache.org/docs/_downloads/65b9451c8de050d7cd9da2fe5a49acc6/deploy_detection.py) + +[下载 Jupyter Notebook:deploy_detection.ipynb](https://tvm.apache.org/docs/_downloads/66e1a42229aae7ed49ac268f520e6727/deploy_detection.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/topic/vta/tutorials/05-conv_opt.md b/versioned_docs/version-0.12.0/topic/vta/tutorials/05-conv_opt.md new file mode 100644 index 00000000..df303859 --- /dev/null +++ b/versioned_docs/version-0.12.0/topic/vta/tutorials/05-conv_opt.md @@ -0,0 +1,755 @@ +--- +title: 2D 卷积优化 +--- + +# 2D 卷积优化 + +:::note +单击 [此处](https://tvm.apache.org/docs/topic/vta/tutorials/optimize/convolution_opt.html#sphx-glr-download-topic-vta-tutorials-optimize-convolution-opt-py) 下载完整的示例代码 +::: + +**作者**:[Thierry Moreau](https://homes.cs.washington.edu/\~moreau/) + +本教程概述了如何用 TVM 在 VTA 设计上有效地映射 2D 卷积工作负载。推荐先学习 [矩阵乘法分块](mat_mul_blocking) 教程。 + +2D 卷积在大多数计算机视觉深度神经网络中占主导地位。本教程将演示 TVM schedule 优化,将 NCHW 布局中的 2D 卷积算子映射到 VTA。还引入了延迟隐藏的概念,使得最大限度地利用 VTA 的计算和内存资源。 + +## RPC 设置 + +首先对 Pynq 的 FPGA 进行编程,并构建其 RPC runtime。 + +``` python +from __future__ import absolute_import, print_function + +import os +import tvm +import tvm.testing +from tvm import te +import vta +import numpy as np + +from tvm import rpc +from tvm.contrib import utils +from vta.testing import simulator + +# 从 3rdparty/vta-hw/config/vta_config.json 文件加载 VTA 参数 +env = vta.get_env() + +# 从 OS 环境中读取 Pynq RPC 主机 IP 地址和端口号 +host = os.environ.get("VTA_RPC_HOST", "192.168.2.99") +port = int(os.environ.get("VTA_RPC_PORT", "9091")) + +# 在 Pynq 上配置比特流和 runtime 系统,匹配 vta_config.json 文件指定的 VTA 配置。 +if env.TARGET == "pynq": + # 确保 TVM 是用 RPC=1 编译的 + assert tvm.runtime.enabled("rpc") + remote = rpc.connect(host, port) + + # 重新配置 JIT runtime + vta.reconfig_runtime(remote) + + # 用预编译的 VTA 比特流对 FPGA 进行编程。 + # 可以通过传递比特流文件的路径而非 None,用自定义比特流对 FPGA 进行编程。 + vta.program_fpga(remote, bitstream=None) + +# 在模拟模式下,本地托管 RPC 服务器。 +elif env.TARGET in ["sim", "tsim"]: + remote = rpc.LocalSession() +``` + +## 计算声明 + +第一步,用 NCHW 格式描述 2D 卷积计算。 + +通过 batch size、空间维度、输入通道、输出通道、内核维度、填充维度和步长维度来定义 2D 卷积 shape。 + +将 ResNet-18 架构的第 9 个卷积层的 shape 作为卷积工作负载参数。 + +在 2D 卷积中添加了额外的算子,这些算子对输出进行移位和裁剪,从而模拟定点卷积,然后进行校正线性激活。下面描述 2D 卷积层的 TVM 数据流图: + +![/img/docs/uwsampl/web-data/main/vta/tutorial/conv2d_dataflow.png](/img/docs/uwsampl/web-data/main/vta/tutorial/conv2d_dataflow.png) + +由于这种计算太大,无法一次全部放入 VTA 的芯片缓冲区。因此,在调度阶段,我们将依靠计算分块策略,将计算分解为易于管理的块。 + +:::note +*空间填充* + +注意,要导入 TOPI 库,在输入特征图张量上应用空间填充。空间填充有助于在 2D 卷积的上下文中进行分块,因为若卷积核窗口大小大于 1,则对于任何给定层的输入特征图,相同的 (x, y) 空间位置会被多次读取。 + +在 CPU 和 GPU 上,并行化工作时提高内存访问效率的一种方法是空间打包,这种方法要对数据进行重新布局。VTA 加载 DMA 引擎可以自动插入填充,因此不必将原始输入特征图重新打包到内存中。 + +我们展示了数据从 DRAM 加载到 VTA 的 SRAM 中时,VTA 的动态空间填充效果。这个过程发生在 2D 跨步和填充内存(strided and padded memory)读取后。 + +![/img/docs/uwsampl/web-data/main/vta/tutorial/padding.png](/img/docs/uwsampl/web-data/main/vta/tutorial/padding.png) +::: + +``` python +from tvm import topi + +# 2D 卷积层尺寸取自 ResNet-18 架构(第 9 个卷积层) +batch_size = 1 +height = 14 +width = 14 +in_channels = 256 +out_channels = 256 +kernel_h = 3 +kernel_w = 3 +pad_h = 1 +pad_w = 1 +stride_h = 1 +stride_w = 1 +assert batch_size % env.BATCH == 0 +assert in_channels % env.BLOCK_IN == 0 +assert out_channels % env.BLOCK_OUT == 0 + +# 输入特征图:(N, IC, H, W, n, ic) +data_shape = ( + batch_size // env.BATCH, + in_channels // env.BLOCK_IN, + height, + width, + env.BATCH, + env.BLOCK_IN, +) +# 内核:(OC,IC,H,W,oc,ic) +kernel_shape = ( + out_channels // env.BLOCK_OUT, + in_channels // env.BLOCK_IN, + kernel_h, + kernel_w, + env.BLOCK_OUT, + env.BLOCK_IN, +) +# 导出输出特征图维度 +fout_height = (height + 2 * pad_h - kernel_h) // stride_h + 1 +fout_width = (width + 2 * pad_w - kernel_w) // stride_w + 1 +# 输出特征图:(N, OC, H, W, n, oc) +output_shape = ( + batch_size // env.BATCH, + out_channels // env.BLOCK_OUT, + fout_height, + fout_width, + env.BATCH, + env.BLOCK_OUT, +) + +# 卷积 reduction 轴 +dy = te.reduce_axis((0, kernel_h), name="dy") +dx = te.reduce_axis((0, kernel_w), name="dx") +ic = te.reduce_axis((0, in_channels // env.BLOCK_IN), name="ic") +ic_tns = te.reduce_axis((0, env.BLOCK_IN), name="ic_tns") + +# 输入占位符张量 +data = te.placeholder(data_shape, name="data", dtype=env.inp_dtype) +kernel = te.placeholder(kernel_shape, name="kernel", dtype=env.wgt_dtype) + +# 复制缓冲区: +# 对输入特征图应用空间填充 +data_buf = topi.nn.pad(data, [0, 0, pad_h, pad_w, 0, 0], name="data_buf") +kernel_buf = te.compute(kernel_shape, lambda *i: kernel(*i), "kernel_buf") + +# 声明二维卷积 +res_conv = te.compute( + output_shape, + lambda bo, co, i, j, bi, ci: te.sum( + data_buf[bo, ic, i * stride_h + dy, j * stride_w + dx, bi, ic_tns].astype(env.acc_dtype) + * kernel_buf[co, ic, dy, dx, ci, ic_tns].astype(env.acc_dtype), + axis=[ic, dy, dx, ic_tns], + ), + name="res_conv", +) + +# 为定点归一化添加移位阶段 +res_shr = te.compute(output_shape, lambda *i: res_conv(*i) >> 8, name="res_shr") + +# 在(0,输入最大值)之间应用 clip 函数 +inp_max = (1 << (env.INP_WIDTH - 1)) - 1 +res_max = te.compute(output_shape, lambda *i: tvm.te.max(res_shr(*i), 0), "res_max") +res_min = te.compute(output_shape, lambda *i: tvm.te.min(res_max(*i), inp_max), "res_min") + +# 结果张量 +res = te.compute(output_shape, lambda *i: res_min(*i).astype(env.inp_dtype), name="res") +``` + +## 调度计算 + +下面将研究用有效方式将 2D 卷积映射到 VTA 所需的一组调度转换。包括: + +* 计算分块 +* 增加计算利用率的虚拟线程 +* 降级到 VTA 硬件内联函数 + +``` python +# 创建 TVM schedule +s = te.create_schedule(res.op) +# 查看默认的 TVM schedule +print(tvm.lower(s, [data, kernel, res], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(data_1: handle, kernel_1: handle, res_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {data: Buffer(data_2: Pointer(int8), int8, [50176], []), + kernel: Buffer(kernel_2: Pointer(int8), int8, [589824], []), + res: Buffer(res_2: Pointer(int8), int8, [50176], [])} + buffer_map = {data_1: data, kernel_1: kernel, res_1: res} + preflattened_buffer_map = {data_1: data_3: Buffer(data_2, int8, [1, 16, 14, 14, 1, 16], []), kernel_1: kernel_3: Buffer(kernel_2, int8, [16, 16, 3, 3, 16, 16], []), res_1: res_3: Buffer(res_2, int8, [1, 16, 14, 14, 1, 16], [])} { + allocate(data_buf: Pointer(global int8), int8, [65536]), storage_scope = global; + allocate(kernel_buf: Pointer(global int8), int8, [589824]), storage_scope = global; + allocate(res_conv: Pointer(global int32), int32, [50176]), storage_scope = global { + for (i1: int32, 0, 16) { + for (i2: int32, 0, 16) { + for (i3: int32, 0, 16) { + for (i5: int32, 0, 16) { + let cse_var_1: int32 = (i3*16) + data_buf_1: Buffer(data_buf, int8, [65536], [])[((((i1*4096) + (i2*256)) + cse_var_1) + i5)] = @tir.if_then_else(((((1 <= i2) && (i2 < 15)) && (1 <= i3)) && (i3 < 15)), data[(((((i1*3136) + (i2*224)) + cse_var_1) + i5) - 240)], 0i8, dtype=int8) + } + } + } + } + for (i0: int32, 0, 16) { + for (i1_1: int32, 0, 16) { + for (i2_1: int32, 0, 3) { + for (i3_1: int32, 0, 3) { + for (i4: int32, 0, 16) { + for (i5_1: int32, 0, 16) { + let cse_var_2: int32 = ((((((i0*36864) + (i1_1*2304)) + (i2_1*768)) + (i3_1*256)) + (i4*16)) + i5_1) + kernel_buf_1: Buffer(kernel_buf, int8, [589824], [])[cse_var_2] = kernel[cse_var_2] + } + } + } + } + } + } + for (co: int32, 0, 16) { + for (i: int32, 0, 14) { + for (j: int32, 0, 14) { + for (ci: int32, 0, 16) { + res_conv_1: Buffer(res_conv, int32, [50176], [])[((((co*3136) + (i*224)) + (j*16)) + ci)] = 0 + for (ic: int32, 0, 16) { + for (dy: int32, 0, 3) { + for (dx: int32, 0, 3) { + for (ic_tns: int32, 0, 16) { + let cse_var_4: int32 = (j*16) + let cse_var_3: int32 = ((((co*3136) + (i*224)) + cse_var_4) + ci) + res_conv_1[cse_var_3] = (res_conv_1[cse_var_3] + (cast(int32, data_buf_1[((((((ic*4096) + (i*256)) + (dy*256)) + cse_var_4) + (dx*16)) + ic_tns)])*cast(int32, kernel_buf_1[((((((co*36864) + (ic*2304)) + (dy*768)) + (dx*256)) + (ci*16)) + ic_tns)]))) + } + } + } + } + } + } + } + } + for (i1_2: int32, 0, 16) { + for (i2_2: int32, 0, 14) { + for (i3_2: int32, 0, 14) { + for (i5_2: int32, 0, 16) { + let cse_var_5: int32 = ((((i1_2*3136) + (i2_2*224)) + (i3_2*16)) + i5_2) + res_conv_2: Buffer(res_conv, int32, [50176], [])[cse_var_5] = @tir.shift_right(res_conv_1[cse_var_5], 8, dtype=int32) + } + } + } + } + for (i1_3: int32, 0, 16) { + for (i2_3: int32, 0, 14) { + for (i3_3: int32, 0, 14) { + for (i5_3: int32, 0, 16) { + let cse_var_6: int32 = ((((i1_3*3136) + (i2_3*224)) + (i3_3*16)) + i5_3) + res_conv_3: Buffer(res_conv, int32, [50176], [])[cse_var_6] = max(res_conv_2[cse_var_6], 0) + } + } + } + } + for (i1_4: int32, 0, 16) { + for (i2_4: int32, 0, 14) { + for (i3_4: int32, 0, 14) { + for (i5_4: int32, 0, 16) { + let cse_var_7: int32 = ((((i1_4*3136) + (i2_4*224)) + (i3_4*16)) + i5_4) + res_conv_4: Buffer(res_conv, int32, [50176], [])[cse_var_7] = min(res_conv_3[cse_var_7], 127) + } + } + } + } + for (i1_5: int32, 0, 16) { + for (i2_5: int32, 0, 14) { + for (i3_5: int32, 0, 14) { + for (i5_5: int32, 0, 16) { + let cse_var_8: int32 = ((((i1_5*3136) + (i2_5*224)) + (i3_5*16)) + i5_5) + res[cse_var_8] = cast(int8, res_conv_4[cse_var_8]) + } + } + } + } + } +} +``` + +### 对计算分块 + +默认情况下,2D 卷积对于激活或内核权重来说太大,无法一次性同时装入 VTA 的芯片缓冲区。沿输入通道、输出通道和高度空间维度分块。不要沿宽度空间维度分块,因为它是 NCHW 布局中的最内层维度(因此,为了增加局部性,最好不要沿最内层维度进行阻塞)。 + +``` python +# 定义平铺大小 +b_block = 1 // env.BATCH +oc_block = 128 // env.BLOCK_OUT +ic_block = 16 // env.BLOCK_IN +h_block = 7 +w_block = 14 + +# 沿空间和输出通道维度平铺输出张量 +# (因为默认进行单个 batch 推理,沿 batch 维度的拆分没有效果) +b, oc, y, x, b_tns, oc_tns = s[res].op.axis +b_out, b_inn = s[res].split(b, factor=b_block) +oc_out, oc_inn = s[res].split(oc, factor=oc_block) +y_out, y_inn = s[res].split(y, factor=h_block) +x_out, x_inn = s[res].split(x, factor=w_block) +s[res].reorder(b_out, oc_out, y_out, x_out, b_inn, oc_inn, y_inn, x_inn, b_tns, oc_tns) + +# 将中间计算移动到每个输出计算块中 +s[res_conv].compute_at(s[res], x_out) +s[res_shr].compute_at(s[res], x_out) +s[res_max].compute_at(s[res], x_out) +s[res_min].compute_at(s[res], x_out) + +# 沿 reduction 轴(输入通道)应用额外的循环分割 +b_inn, oc_inn, y_inn, x_inn, b_tns, oc_tns = s[res_conv].op.axis +ic_out, ic_inn = s[res_conv].split(ic, factor=ic_block) + +# 对轴重新排序。 +# 1)在最里面的位置将 VTA 张量轴分组:b_tns、oc_tns、ic_tns,使得 TVM 张量化。 +# 2)将 ic_out 轴移出卷积循环,沿 reduction 轴阻塞。 +# 3)现在对块轴重新排序:b_inn、oc_inn、y_inn、x_inn、ic_inn、dy、dx。 +# VTA runtime/硬件要求为每个 VTA 张量操作写入不同的输出特征图位置。 +# 这个限制要求在 b_tns 之前对 oc_inn、y_inn 或 x_inn 其中之一进行排序,因为它们都会影响输出特征图索引。 +# 下面将 x_inn 放进去。 +s[res_conv].reorder(ic_out, b_inn, oc_inn, y_inn, ic_inn, dy, dx, x_inn, b_tns, oc_tns, ic_tns) +``` + +### 虚拟线程 + +虚拟线程是 VTA 硬件设计中,一种提高任务级 pipeline 并行性的机制。换言之,它通过隐藏内存访问延迟,来提高计算资源利用率。 + +以下实现,虚拟线程将工作分配给沿输出通道轴拆分的两个线程。下图展示了计算 2D 卷积时,工作是如何划分的。 + +![/img/docs/uwsampl/web-data/main/vta/tutorial/virtual_threading.png](/img/docs/uwsampl/web-data/main/vta/tutorial/virtual_threading.png) + +``` python +# VTA 只支持 2 个虚拟线程 +v_threads = 2 + +# 沿输出通道外轴进行虚拟线程拆分 +_, tx = s[res].split(oc_out, factor=v_threads) +s[res].reorder(tx, b_out) +s[res].bind(tx, te.thread_axis("cthread")) + +# 查看阻塞和虚拟线程后的当前 TVM schedule +print(tvm.lower(s, [data, kernel, res], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(data_1: handle, kernel_1: handle, res_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {data: Buffer(data_2: Pointer(int8), int8, [50176], []), + kernel: Buffer(kernel_2: Pointer(int8), int8, [589824], []), + res: Buffer(res_2: Pointer(int8), int8, [50176], [])} + buffer_map = {data_1: data, kernel_1: kernel, res_1: res} + preflattened_buffer_map = {data_1: data_3: Buffer(data_2, int8, [1, 16, 14, 14, 1, 16], []), kernel_1: kernel_3: Buffer(kernel_2, int8, [16, 16, 3, 3, 16, 16], []), res_1: res_3: Buffer(res_2, int8, [1, 16, 14, 14, 1, 16], [])} { + allocate(data_buf: Pointer(global int8), int8, [65536]), storage_scope = global; + allocate(kernel_buf: Pointer(global int8), int8, [589824]), storage_scope = global; + allocate(res_conv: Pointer(global int32), int32, [25088]), storage_scope = global { + for (i1: int32, 0, 16) { + for (i2: int32, 0, 16) { + for (i3: int32, 0, 16) { + for (i5: int32, 0, 16) { + let cse_var_1: int32 = (i3*16) + data_buf_1: Buffer(data_buf, int8, [65536], [])[((((i1*4096) + (i2*256)) + cse_var_1) + i5)] = @tir.if_then_else(((((1 <= i2) && (i2 < 15)) && (1 <= i3)) && (i3 < 15)), data[(((((i1*3136) + (i2*224)) + cse_var_1) + i5) - 240)], 0i8, dtype=int8) + } + } + } + } + for (i0: int32, 0, 16) { + for (i1_1: int32, 0, 16) { + for (i2_1: int32, 0, 3) { + for (i3_1: int32, 0, 3) { + for (i4: int32, 0, 16) { + for (i5_1: int32, 0, 16) { + let cse_var_2: int32 = ((((((i0*36864) + (i1_1*2304)) + (i2_1*768)) + (i3_1*256)) + (i4*16)) + i5_1) + kernel_buf_1: Buffer(kernel_buf, int8, [589824], [])[cse_var_2] = kernel[cse_var_2] + } + } + } + } + } + } + for (i2.outer: int32, 0, 2) { + for (co.init: int32, 0, 8) { + for (i.init: int32, 0, 7) { + for (j.init: int32, 0, 14) { + for (ci.init: int32, 0, 16) { + let cse_var_3: int32 = ((((co.init*1568) + (i.init*224)) + (j.init*16)) + ci.init) + { + res_conv_1: Buffer(res_conv, int32, [157351936], [])[cse_var_3] = 0 + res_conv_1[(cse_var_3 + 12544)] = 0 + } + } + } + } + } + for (ic.outer: int32, 0, 16) { + for (co: int32, 0, 8) { + for (i: int32, 0, 7) { + for (dy: int32, 0, 3) { + for (dx: int32, 0, 3) { + for (j: int32, 0, 14) { + for (ci: int32, 0, 16) { + for (ic_tns: int32, 0, 16) { + let cse_var_8: int32 = (j*16) + let cse_var_7: int32 = ((((co*1568) + (i*224)) + cse_var_8) + ci) + let cse_var_6: int32 = (cse_var_7 + 12544) + let cse_var_5: int32 = ((((((co*36864) + (ic.outer*2304)) + (dy*768)) + (dx*256)) + (ci*16)) + ic_tns) + let cse_var_4: int32 = (((((((ic.outer*4096) + (i2.outer*1792)) + (i*256)) + (dy*256)) + cse_var_8) + (dx*16)) + ic_tns) + { + res_conv_1[cse_var_7] = (res_conv_1[cse_var_7] + (cast(int32, data_buf_1[cse_var_4])*cast(int32, kernel_buf_1[cse_var_5]))) + res_conv_1[cse_var_6] = (res_conv_1[cse_var_6] + (cast(int32, data_buf_1[cse_var_4])*cast(int32, kernel_buf_1[(cse_var_5 + 294912)]))) + } + } + } + } + } + } + } + } + } + for (i1_2: int32, 0, 8) { + for (i2_2: int32, 0, 7) { + for (i3_2: int32, 0, 14) { + for (i5_2: int32, 0, 16) { + let cse_var_10: int32 = ((((i1_2*1568) + (i2_2*224)) + (i3_2*16)) + i5_2) + let cse_var_9: int32 = (cse_var_10 + 12544) + { + res_conv_2: Buffer(res_conv, int32, [157351936], [])[cse_var_10] = @tir.shift_right(res_conv_1[cse_var_10], 8, dtype=int32) + res_conv_2[cse_var_9] = @tir.shift_right(res_conv_1[cse_var_9], 8, dtype=int32) + } + } + } + } + } + for (i1_3: int32, 0, 8) { + for (i2_3: int32, 0, 7) { + for (i3_3: int32, 0, 14) { + for (i5_3: int32, 0, 16) { + let cse_var_12: int32 = ((((i1_3*1568) + (i2_3*224)) + (i3_3*16)) + i5_3) + let cse_var_11: int32 = (cse_var_12 + 12544) + { + res_conv_3: Buffer(res_conv, int32, [157351936], [])[cse_var_12] = max(res_conv_2[cse_var_12], 0) + res_conv_3[cse_var_11] = max(res_conv_2[cse_var_11], 0) + } + } + } + } + } + for (i1_4: int32, 0, 8) { + for (i2_4: int32, 0, 7) { + for (i3_4: int32, 0, 14) { + for (i5_4: int32, 0, 16) { + let cse_var_14: int32 = ((((i1_4*1568) + (i2_4*224)) + (i3_4*16)) + i5_4) + let cse_var_13: int32 = (cse_var_14 + 12544) + { + res_conv_4: Buffer(res_conv, int32, [157351936], [])[cse_var_14] = min(res_conv_3[cse_var_14], 127) + res_conv_4[cse_var_13] = min(res_conv_3[cse_var_13], 127) + } + } + } + } + } + for (i1.inner: int32, 0, 8) { + for (i2.inner: int32, 0, 7) { + for (i3.inner: int32, 0, 14) { + for (i5_5: int32, 0, 16) { + let cse_var_18: int32 = (i2.inner*224) + let cse_var_17: int32 = (i3.inner*16) + let cse_var_16: int32 = ((((i1.inner*1568) + cse_var_18) + cse_var_17) + i5_5) + let cse_var_15: int32 = (((((i1.inner*3136) + (i2.outer*1568)) + cse_var_18) + cse_var_17) + i5_5) + { + res[cse_var_15] = cast(int8, res_conv_4[cse_var_16]) + res[(cse_var_15 + 25088)] = cast(int8, res_conv_4[(cse_var_16 + 12544)]) + } + } + } + } + } + } + } +} +``` + +### 将拷贝降级到 DMA 传输 + +接下来,将缓冲区范围设置为相应的芯片 VTA SRAM 缓冲区。将加载循环移动到 2D 卷积计算循环中,暂存内存加载,使其适合芯片上 SRAM buffer。最后,用 DMA 拷贝编译指示来注释加载/存储循环外轴,从而在 VTA 上执行大容量内存传输。 + +``` python +# 设置 SRAM buffer 的范围 +s[data_buf].set_scope(env.inp_scope) +s[kernel_buf].set_scope(env.wgt_scope) +s[res_conv].set_scope(env.acc_scope) +s[res_shr].set_scope(env.acc_scope) +s[res_min].set_scope(env.acc_scope) +s[res_max].set_scope(env.acc_scope) + +# 块数据和内核缓存读取 +s[data_buf].compute_at(s[res_conv], ic_out) +s[kernel_buf].compute_at(s[res_conv], ic_out) + +# 用 DMA 拷贝编译指示操作 DRAM->SRAM +s[data_buf].pragma(s[data_buf].op.axis[0], env.dma_copy) +s[kernel_buf].pragma(s[kernel_buf].op.axis[0], env.dma_copy) + +# 在每个结果块中的 SRAM->DRAM 操作上,使用 DMA 拷贝编译指示(这意味着这些拷贝应沿 b_inn 或结果轴 4 执行) +s[res].pragma(s[res].op.axis[4], env.dma_copy) +``` + +### 将计算降级为 VTA 计算内联函数 + +最后一个阶段是降级计算循环到 VTA 硬件内联函数,这是通过将 2D 卷积映射到张量内联函数,并将移位和裁剪计算映射到向量 ALU 实现的。 + +``` python +# 在 batch 张量平铺轴上应用张量化 +s[res_conv].tensorize(b_tns, env.gemm) + +# 在移位和裁剪操作上添加 ALU 编译指示 +s[res_shr].pragma(s[res_shr].op.axis[0], env.alu) +s[res_min].pragma(s[res_min].op.axis[0], env.alu) +s[res_max].pragma(s[res_max].op.axis[0], env.alu) + +# 将内存负载/存储降级为 DMA 拷贝内联函数,并将计算降级为 VTA 计算内联函数后,查看最终降级的 TVM schedule。 +print(vta.lower(s, [data, kernel, res], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(data_1: handle, kernel_1: handle, res_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {data: Buffer(data_2: Pointer(int8), int8, [50176], []), + kernel: Buffer(kernel_2: Pointer(int8), int8, [589824], []), + res: Buffer(res_2: Pointer(int8), int8, [50176], [])} + buffer_map = {data_1: data, kernel_1: kernel, res_1: res} + preflattened_buffer_map = {data_1: data_3: Buffer(data_2, int8, [1, 16, 14, 14, 1, 16], []), kernel_1: kernel_3: Buffer(kernel_2, int8, [16, 16, 3, 3, 16, 16], []), res_1: res_3: Buffer(res_2, int8, [1, 16, 14, 14, 1, 16], [])} { + @tir.vta.coproc_dep_push(3, 2, dtype=int32) + @tir.vta.coproc_dep_push(3, 2, dtype=int32) + for (i2.outer: int32, 0, 2) { + for (cthread.s: int32, 0, 2) { + attr [IterVar(vta: int32, (nullptr), "ThreadIndex", "vta")] "coproc_scope" = 2 { + @tir.vta.coproc_dep_pop(3, 2, dtype=int32) + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_uop_scope" = "VTAPushGEMMOp" { + @tir.call_extern("VTAUopLoopBegin", 8, 98, 0, 0, dtype=int32) + @tir.call_extern("VTAUopLoopBegin", 7, 14, 0, 0, dtype=int32) + for (j.init: int32, 0, 14) { + @tir.vta.uop_push(0, 1, ((cthread.s*784) + j.init), 0, 0, 0, 0, 0, dtype=int32) + } + @tir.call_extern("VTAUopLoopEnd", dtype=int32) + @tir.call_extern("VTAUopLoopEnd", dtype=int32) + } + @tir.vta.coproc_dep_push(2, 1, dtype=int32) + } + } + for (ic.outer: int32, 0, 16) { + let cse_var_6: int32 = (i2.outer*7) + let cse_var_5: int32 = (ic.outer*9) + let cse_var_4: int32 = max((1 - cse_var_6), 0) + let cse_var_3: int32 = max((cse_var_6 - 6), 0) + let cse_var_2: int32 = ((9 - cse_var_4) - cse_var_3) + let cse_var_1: int32 = ((((ic.outer*196) + (i2.outer*98)) + (cse_var_4*14)) - 14) + { + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_scope" = 1 { + @tir.vta.coproc_dep_pop(2, 1, dtype=int32) + @tir.call_extern("VTALoadBuffer2D", @tir.tvm_thread_context(@tir.vta.command_handle(, dtype=handle), dtype=handle), data_2, cse_var_1, 14, cse_var_2, 14, 1, cse_var_4, 1, cse_var_3, 0, 2, dtype=int32) + @tir.call_extern("VTALoadBuffer2D", @tir.tvm_thread_context(@tir.vta.command_handle(, dtype=handle), dtype=handle), kernel_2, cse_var_5, 9, 8, 144, 0, 0, 0, 0, 0, 1, dtype=int32) + @tir.vta.coproc_dep_push(1, 2, dtype=int32) + } + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_scope" = 1 { + @tir.vta.coproc_dep_pop(2, 1, dtype=int32) + @tir.call_extern("VTALoadBuffer2D", @tir.tvm_thread_context(@tir.vta.command_handle(, dtype=handle), dtype=handle), data_2, cse_var_1, 14, cse_var_2, 14, 1, cse_var_4, 1, cse_var_3, 144, 2, dtype=int32) + @tir.call_extern("VTALoadBuffer2D", @tir.tvm_thread_context(@tir.vta.command_handle(, dtype=handle), dtype=handle), kernel_2, (cse_var_5 + 1152), 9, 8, 144, 0, 0, 0, 0, 72, 1, dtype=int32) + @tir.vta.coproc_dep_push(1, 2, dtype=int32) + } + for (cthread.s_1: int32, 0, 2) { + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_scope" = 2 { + @tir.vta.coproc_dep_pop(1, 2, dtype=int32) + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_uop_scope" = "VTAPushGEMMOp" { + @tir.call_extern("VTAUopLoopBegin", 8, 98, 0, 9, dtype=int32) + @tir.call_extern("VTAUopLoopBegin", 7, 14, 16, 0, dtype=int32) + for (dy: int32, 0, 3) { + for (dx: int32, 0, 3) { + for (j: int32, 0, 14) { + @tir.vta.uop_push(0, 0, ((cthread.s_1*784) + j), ((((cthread.s_1*144) + (dy*16)) + j) + dx), (((cthread.s_1*72) + (dy*3)) + dx), 0, 0, 0, dtype=int32) + } + } + } + @tir.call_extern("VTAUopLoopEnd", dtype=int32) + @tir.call_extern("VTAUopLoopEnd", dtype=int32) + } + @tir.vta.coproc_dep_push(2, 1, dtype=int32) + } + } + } + } + @tir.vta.coproc_dep_pop(2, 1, dtype=int32) + @tir.vta.coproc_dep_pop(2, 1, dtype=int32) + for (cthread.s_2: int32, 0, 2) { + let cse_var_7: int32 = (cthread.s_2*784) + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_scope" = 2 { + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_uop_scope" = "VTAPushALUOp" { + @tir.call_extern("VTAUopLoopBegin", 784, 1, 1, 0, dtype=int32) + @tir.vta.uop_push(1, 0, cse_var_7, cse_var_7, 0, 3, 1, 8, dtype=int32) + @tir.call_extern("VTAUopLoopEnd", dtype=int32) + } + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_uop_scope" = "VTAPushALUOp" { + @tir.call_extern("VTAUopLoopBegin", 784, 1, 1, 0, dtype=int32) + @tir.vta.uop_push(1, 0, cse_var_7, cse_var_7, 0, 1, 1, 0, dtype=int32) + @tir.call_extern("VTAUopLoopEnd", dtype=int32) + } + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_uop_scope" = "VTAPushALUOp" { + @tir.call_extern("VTAUopLoopBegin", 784, 1, 1, 0, dtype=int32) + @tir.vta.uop_push(1, 0, cse_var_7, cse_var_7, 0, 0, 1, 127, dtype=int32) + @tir.call_extern("VTAUopLoopEnd", dtype=int32) + } + @tir.vta.coproc_dep_push(2, 3, dtype=int32) + } + } + for (cthread.s_3: int32, 0, 2) { + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_scope" = 3 { + @tir.vta.coproc_dep_pop(2, 3, dtype=int32) + for (i1.inner: int32, 0, 8) { + for (i2.inner: int32, 0, 7) { + for (i3.inner: int32, 0, 14) { + let cse_var_8: int32 = (i2.inner*14) + @tir.call_extern("VTAStoreBuffer2D", @tir.tvm_thread_context(@tir.vta.command_handle(, dtype=handle), dtype=handle), ((((cthread.s_3*784) + (i1.inner*98)) + cse_var_8) + i3.inner), 4, res_2, (((((cthread.s_3*1568) + (i1.inner*196)) + (i2.outer*98)) + cse_var_8) + i3.inner), 1, 1, 1, dtype=int32) + } + } + } + @tir.vta.coproc_dep_push(3, 2, dtype=int32) + } + } + } + @tir.vta.coproc_dep_pop(3, 2, dtype=int32) + @tir.vta.coproc_dep_pop(3, 2, dtype=int32) + @tir.vta.coproc_sync(, dtype=int32) +} +``` + +## TVM 编译及验证 + +指定 schedule 后,可以将其编译为 TVM 函数。保存模块,以便通过 RPC 发送。运行这个函数,并根据 numpy 实现对其进行验证,以确保正确性。 + +``` python +# 用这个库进行 2D 卷积测试 +from tvm.topi.testing import conv2d_nchw_python + +# 编译 TVM 模块 +with vta.build_config(disabled_pass={"tir.CommonSubexprElimTIR"}): + my_conv = vta.build( + s, [data, kernel, res], tvm.target.Target("ext_dev", host=env.target_host), name="my_conv" + ) +temp = utils.tempdir() +my_conv.save(temp.relpath("conv2d.o")) +remote.upload(temp.relpath("conv2d.o")) +f = remote.load_module("conv2d.o") + +# 获取远程设备上下文 +ctx = remote.ext_dev(0) + +# 在 NCHW 布局的 (-128, 128] int 范围内随机初始化数据和内核数组 +data_np = np.random.randint(-128, 128, size=(batch_size, in_channels, height, width)).astype( + data.dtype +) +kernel_np = np.random.randint( + -128, 128, size=(out_channels, in_channels, kernel_h, kernel_w) +).astype(kernel.dtype) + +# 将数据和内核数组从 2D NCHW 打包为 4D NCHWnc 打包布局 +data_packed = data_np.reshape( + batch_size // env.BATCH, env.BATCH, in_channels // env.BLOCK_IN, env.BLOCK_IN, height, width +).transpose((0, 2, 4, 5, 1, 3)) + +kernel_packed = kernel_np.reshape( + out_channels // env.BLOCK_OUT, + env.BLOCK_OUT, + in_channels // env.BLOCK_IN, + env.BLOCK_IN, + kernel_h, + kernel_w, +).transpose((0, 2, 4, 5, 1, 3)) + +# 用 tvm.nd.array 将输入/输出数组格式化为 DLPack 标准 +data_nd = tvm.nd.array(data_packed, ctx) +kernel_nd = tvm.nd.array(kernel_packed, ctx) +res_nd = tvm.nd.array(np.zeros(output_shape).astype(res.dtype), ctx) + +# 清除统计 +if env.TARGET in ["sim", "tsim"]: + simulator.clear_stats() + +# 调用模块进行计算 +f(data_nd, kernel_nd, res_nd) + +# 针对 numpy 实现进行验证 +res_ref = conv2d_nchw_python( + data_np.astype(env.acc_dtype), + kernel_np.astype(env.acc_dtype), + (stride_h, stride_w), + (pad_h, pad_w), +).astype(env.acc_dtype) +res_ref = res_ref >> env.INP_WIDTH +res_ref = np.clip(res_ref, 0, inp_max) +res_ref = res_ref.astype(res.dtype) +res_ref = res_ref.reshape( + ( + batch_size // env.BATCH, + env.BATCH, + out_channels // env.BLOCK_OUT, + env.BLOCK_OUT, + fout_height, + fout_width, + ) +).transpose((0, 2, 4, 5, 1, 3)) +tvm.testing.assert_allclose(res_ref, res_nd.numpy()) + +# 打印统计 +if env.TARGET in ["sim", "tsim"]: + sim_stats = simulator.stats() + print("Execution statistics:") + for k, v in sim_stats.items(): + print("\t{:<16}: {:>16}".format(k, v)) + +print("Successful 2D convolution test!") +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:267: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +Execution statistics: + inp_load_nbytes : 114688 + wgt_load_nbytes : 1179648 + acc_load_nbytes : 0 + uop_load_nbytes : 1144 + out_store_nbytes: 50176 + gemm_counter : 451584 + alu_counter : 9408 +Successful 2D convolution test! +``` + +## 总结 + +本教程演示如何用 TVM 调度原语将 2D 卷积降级到硬件加速器内联函数上,利用硬件特定的优化,例如使用虚拟线程延迟隐藏。 + +[下载 Python 源代码:convolution_opt.py](https://tvm.apache.org/docs/_downloads/13ef71e33eaef0855c6e883d9ec5d632/convolution_opt.py) + +[下载 Jupyter Notebook:convolution_opt.ipynb](https://tvm.apache.org/docs/_downloads/b3f997c945cc7de3e03a1e0c4c73fabd/convolution_opt.ipynb) diff --git a/versioned_docs/version-0.12.0/topic/vta/tutorials/06-mat_mul_blocking.md b/versioned_docs/version-0.12.0/topic/vta/tutorials/06-mat_mul_blocking.md new file mode 100644 index 00000000..efb925e9 --- /dev/null +++ b/versioned_docs/version-0.12.0/topic/vta/tutorials/06-mat_mul_blocking.md @@ -0,0 +1,556 @@ +--- +title: 矩阵乘法分块 +--- + +# 矩阵乘法分块 + +:::note +单击 [此处](https://tvm.apache.org/docs/topic/vta/tutorials/optimize/matrix_multiply_opt.html#sphx-glr-download-topic-vta-tutorials-optimize-matrix-multiply-opt-py) 下载完整的示例代码 +::: + +**作者**:[Thierry Moreau](https://homes.cs.washington.edu/\~moreau/) + +本教程概述了如何用 TVM 在 VTA 设计上有效地映射矩阵乘法。推荐先学习 [简单矩阵乘法](mat_mul) 教程。 + +本教程演示 TVM 调度优化,将大型神经网络算子分解为更小的块,使得可以在有限的硬件加速器资源内实现计算。 + +## RPC 设置 + +首先对 Pynq 的 FPGA 进行编程,并构建其 RPC runtime。 + +``` python +from __future__ import absolute_import, print_function + +import os +import tvm +from tvm import te +import vta +import numpy as np +from tvm import rpc +from tvm.contrib import utils +from vta.testing import simulator + +# 从 3rdparty/vta-hw/config/vta_config.json 文件加载 VTA 参数 +env = vta.get_env() + +# 从 OS 环境中读取 Pynq RPC 主机 IP 地址和端口号 +host = os.environ.get("VTA_RPC_HOST", "192.168.2.99") +port = int(os.environ.get("VTA_RPC_PORT", "9091")) + +# 在 Pynq 上配置比特流和 runtime 系统,匹配 vta_config.json 文件指定的 VTA 配置。 +if env.TARGET == "pynq": + # 确保 TVM 是用 RPC=1 编译的 + assert tvm.runtime.enabled("rpc") + remote = rpc.connect(host, port) + + # 重新配置 JIT runtime + vta.reconfig_runtime(remote) + + # 用预编译的 VTA 比特流对 FPGA 进行编程。 + # 可以通过传递比特流文件的路径而非 None,用自定义比特流对 FPGA 进行编程。 + vta.program_fpga(remote, bitstream=None) + +# 在模拟模式下,本地托管 RPC 服务器。 +elif env.TARGET in ["sim", "tsim"]: + remote = rpc.LocalSession() +``` + +## 计算声明 + +第一步,描述矩阵乘法计算。 + +将矩阵乘法定义为全连接层中可以找到的计算,由其 batch size、输入通道和输出通道定义。它们必须是 VTA 张量 shape 的整数倍:分别为 `BATCH`、`BLOCK_IN` 和 `BLOCK_OUT`。 + +在矩阵乘法中添加了额外的算子,这些算子对输出进行移位和裁剪,从而模拟定点卷积,然后进行校正线性激活。下面描述全连接层的 TVM 数据流图: + +![/img/docs/uwsampl/web-data/main/vta/tutorial/fc_dataflow.png](/img/docs/uwsampl/web-data/main/vta/tutorial/fc_dataflow.png) + +由于这种计算太大,无法一次全部放入 VTA 的芯片缓冲区。因此,在调度阶段,依靠计算分块策略将计算分解为可管理的块。 + +``` python +# 全连接层维度:1024 x 1024 +batch_size = 1 +in_channels = 1024 +out_channels = 1024 +assert batch_size % env.BATCH == 0 +assert in_channels % env.BLOCK_IN == 0 +assert out_channels % env.BLOCK_OUT == 0 + +# 推导平铺的输入张量 shape +data_shape = (batch_size // env.BATCH, in_channels // env.BLOCK_IN, env.BATCH, env.BLOCK_IN) +weight_shape = ( + out_channels // env.BLOCK_OUT, + in_channels // env.BLOCK_IN, + env.BLOCK_OUT, + env.BLOCK_IN, +) +output_shape = (batch_size // env.BATCH, out_channels // env.BLOCK_OUT, env.BATCH, env.BLOCK_OUT) +num_ops = in_channels * out_channels * batch_size * 2 + +# Reduction 轴 +ic = te.reduce_axis((0, in_channels // env.BLOCK_IN), name="ic") +ic_tns = te.reduce_axis((0, env.BLOCK_IN), name="ic_tns") + +# 输入占位符张量 +data = te.placeholder(data_shape, name="data", dtype=env.inp_dtype) +weight = te.placeholder(weight_shape, name="weight", dtype=env.wgt_dtype) + +# 复制缓冲区 +data_buf = te.compute(data_shape, lambda *i: data(*i), "data_buf") +weight_buf = te.compute(weight_shape, lambda *i: weight(*i), "weight_buf") + +# 声明矩阵乘法计算 +res_gemm = te.compute( + output_shape, + lambda bo, co, bi, ci: te.sum( + data_buf[bo, ic, bi, ic_tns].astype(env.acc_dtype) + * weight_buf[co, ic, ci, ic_tns].astype(env.acc_dtype), + axis=[ic, ic_tns], + ), + name="res_gem", +) + +# 为定点归一化添加移位阶段 +res_shr = te.compute(output_shape, lambda *i: res_gemm(*i) >> env.INP_WIDTH, name="res_shr") + +# 在(0,输入最大值)之间应用裁剪 +inp_max = (1 << (env.INP_WIDTH - 1)) - 1 +res_max = te.compute(output_shape, lambda *i: tvm.te.max(res_shr(*i), 0), "res_max") +res_min = te.compute(output_shape, lambda *i: tvm.te.min(res_max(*i), inp_max), "res_min") + +# 返回结果前,对输入数据类型进行类型转换 +res = te.compute(output_shape, lambda *i: res_min(*i).astype(env.inp_dtype), name="res") +``` + +## 调度计算 + +下面将研究用有效方式将矩阵乘法映射到 VTA 所需的一组调度转换。包括: + +* 计算分块 +* 降级到 VTA 硬件内联函数 + +``` python +# 创建 TVM schedule +s = te.create_schedule(res.op) +# 查看默认的 TVM schedule +print(tvm.lower(s, [data, weight, res], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(data_1: handle, weight_1: handle, res_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {data: Buffer(data_2: Pointer(int8), int8, [1024], []), + weight: Buffer(weight_2: Pointer(int8), int8, [1048576], []), + res: Buffer(res_2: Pointer(int8), int8, [1024], [])} + buffer_map = {data_1: data, weight_1: weight, res_1: res} + preflattened_buffer_map = {data_1: data_3: Buffer(data_2, int8, [1, 64, 1, 16], []), weight_1: weight_3: Buffer(weight_2, int8, [64, 64, 16, 16], []), res_1: res_3: Buffer(res_2, int8, [1, 64, 1, 16], [])} { + allocate(data_buf: Pointer(global int8), int8, [1024]), storage_scope = global; + allocate(weight_buf: Pointer(global int8), int8, [1048576]), storage_scope = global; + allocate(res_gem: Pointer(global int32), int32, [1024]), storage_scope = global { + for (i1: int32, 0, 64) { + for (i3: int32, 0, 16) { + let cse_var_1: int32 = ((i1*16) + i3) + data_buf_1: Buffer(data_buf, int8, [1024], [])[cse_var_1] = data[cse_var_1] + } + } + for (i0: int32, 0, 64) { + for (i1_1: int32, 0, 64) { + for (i2: int32, 0, 16) { + for (i3_1: int32, 0, 16) { + let cse_var_2: int32 = ((((i0*16384) + (i1_1*256)) + (i2*16)) + i3_1) + weight_buf_1: Buffer(weight_buf, int8, [1048576], [])[cse_var_2] = weight[cse_var_2] + } + } + } + } + for (co: int32, 0, 64) { + for (ci: int32, 0, 16) { + res_gem_1: Buffer(res_gem, int32, [1024], [])[((co*16) + ci)] = 0 + for (ic: int32, 0, 64) { + for (ic_tns: int32, 0, 16) { + let cse_var_3: int32 = ((co*16) + ci) + res_gem_1[cse_var_3] = (res_gem_1[cse_var_3] + (cast(int32, data_buf_1[((ic*16) + ic_tns)])*cast(int32, weight_buf_1[((((co*16384) + (ic*256)) + (ci*16)) + ic_tns)]))) + } + } + } + } + for (i1_2: int32, 0, 64) { + for (i3_2: int32, 0, 16) { + let cse_var_4: int32 = ((i1_2*16) + i3_2) + res_gem_2: Buffer(res_gem, int32, [1024], [])[cse_var_4] = @tir.shift_right(res_gem_1[cse_var_4], 8, dtype=int32) + } + } + for (i1_3: int32, 0, 64) { + for (i3_3: int32, 0, 16) { + let cse_var_5: int32 = ((i1_3*16) + i3_3) + res_gem_3: Buffer(res_gem, int32, [1024], [])[cse_var_5] = max(res_gem_2[cse_var_5], 0) + } + } + for (i1_4: int32, 0, 64) { + for (i3_4: int32, 0, 16) { + let cse_var_6: int32 = ((i1_4*16) + i3_4) + res_gem_4: Buffer(res_gem, int32, [1024], [])[cse_var_6] = min(res_gem_3[cse_var_6], 127) + } + } + for (i1_5: int32, 0, 64) { + for (i3_5: int32, 0, 16) { + let cse_var_7: int32 = ((i1_5*16) + i3_5) + res[cse_var_7] = cast(int8, res_gem_4[cse_var_7]) + } + } + } +} +``` + +### 对计算分块 + +默认情况下,2D 卷积对于激活或内核权重来说太大,无法一次性同时装入 VTA 的芯片缓冲区。将 (1, 1024) x (1024, 1024) 矩阵乘法分块为更小的 (1, 256) x (256, 256) 矩阵乘法,使得中间张量可以适合加速器的芯片上 SRAM。这种方法类似于为提高缓存命中率,应用于 CPU 和 GPU 的分块技术。 + +沿每个轴执行分块(由于正在执行单个 batch 推理,batch 轴未被处理)。还将最里面的张量轴保持原样,使得 TVM 模式匹配张量。下图展示了计算调度上的分块结果: + +![/img/docs/uwsampl/web-data/main/vta/tutorial/blocking.png](/img/docs/uwsampl/web-data/main/vta/tutorial/blocking.png) + +:::note +循环拆分和重新排序后的代码等价于以下伪代码。由于以下示例只执行单个 batch 推理,因此将忽略 batch 轴: + +``` python +for (int oc_out = 0; oc_out < 4; ++oc_out) { + // Initialization loop + // 初始化循环 + for (int oc_inn = 0; oc_inn < 16; ++oc_inn) { + for (int oc_tns = 0; oc_tns < 16; ++oc_tns) { + int j = (oc_out * 16 + oc_inn) * 16 + oc_tns; + C[0][j] = 0; + } + } + for (int ic_out = 0; ic_out < 4; ++ic_out) { + // Block loop + // 块循环 + for (int oc_inn = 0; oc_inn < 16; ++oc_inn) { + for (int ic_inn = 0; ic_inn < 16; ++ic_inn) { + // Tensorization loop + // 张量循环 + for (int oc_tns = 0; oc_tns < 16; ++oc_tns) { + for (int ic_tns = 0; ic_tns < 16; ++ic_tns) { + int i = (ic_out * 16 + ic_inn) * 16 + ic_tns; + int j = (oc_out * 16 + oc_inn) * 16 + oc_tns; + C[0][i] = C[0][i] + A[0][i] * B[j][i]; + } + } + } + } + } + } +} +``` +::: + +``` python +# 定义平铺大小(用 VTA 张量 shape 大小的倍数表示) +b_block = 1 // env.BATCH +i_block = 256 // env.BLOCK_IN +o_block = 256 // env.BLOCK_OUT + +# 沿空间和输出通道维度平铺输出张量 +# (因为默认进行单个 batch 推理,沿 batch 维度的拆分没有效果) +b, oc, b_tns, oc_tns = s[res].op.axis +b_out, b_inn = s[res].split(b, b_block) +oc_out, oc_inn = s[res].split(oc, o_block) +s[res].reorder(b_out, oc_out, b_inn, oc_inn) + +# 将中间计算移动到每个输出计算块中 +s[res_gemm].compute_at(s[res], oc_out) +s[res_shr].compute_at(s[res], oc_out) +s[res_max].compute_at(s[res], oc_out) +s[res_min].compute_at(s[res], oc_out) + +# 沿 reduction 轴(输入通道)应用额外的循环分割 +b_inn, oc_inn, b_tns, oc_tns = s[res_gemm].op.axis +ic_out, ic_inn = s[res_gemm].split(ic, i_block) + +# 对轴重新排序。将 ic_out 轴移出卷积循环,沿 reduction 轴阻塞。 +s[res_gemm].reorder(ic_out, b_inn, oc_inn, ic_inn, b_tns, oc_tns, ic_tns) + +# 查看阻塞后的当前 TVM schedule +print(tvm.lower(s, [data, weight, res], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(data_1: handle, weight_1: handle, res_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {data: Buffer(data_2: Pointer(int8), int8, [1024], []), + weight: Buffer(weight_2: Pointer(int8), int8, [1048576], []), + res: Buffer(res_2: Pointer(int8), int8, [1024], [])} + buffer_map = {data_1: data, weight_1: weight, res_1: res} + preflattened_buffer_map = {data_1: data_3: Buffer(data_2, int8, [1, 64, 1, 16], []), weight_1: weight_3: Buffer(weight_2, int8, [64, 64, 16, 16], []), res_1: res_3: Buffer(res_2, int8, [1, 64, 1, 16], [])} { + allocate(data_buf: Pointer(global int8), int8, [1024]), storage_scope = global; + allocate(weight_buf: Pointer(global int8), int8, [1048576]), storage_scope = global; + allocate(res_gem: Pointer(global int32), int32, [256]), storage_scope = global { + for (i1: int32, 0, 64) { + for (i3: int32, 0, 16) { + let cse_var_1: int32 = ((i1*16) + i3) + data_buf_1: Buffer(data_buf, int8, [1024], [])[cse_var_1] = data[cse_var_1] + } + } + for (i0: int32, 0, 64) { + for (i1_1: int32, 0, 64) { + for (i2: int32, 0, 16) { + for (i3_1: int32, 0, 16) { + let cse_var_2: int32 = ((((i0*16384) + (i1_1*256)) + (i2*16)) + i3_1) + weight_buf_1: Buffer(weight_buf, int8, [1048576], [])[cse_var_2] = weight[cse_var_2] + } + } + } + } + for (i1.outer: int32, 0, 4) { + for (co.init: int32, 0, 16) { + for (ci.init: int32, 0, 16) { + res_gem_1: Buffer(res_gem, int32, [256], [])[((co.init*16) + ci.init)] = 0 + } + } + for (ic.outer: int32, 0, 4) { + for (co: int32, 0, 16) { + for (ic.inner: int32, 0, 16) { + for (ci: int32, 0, 16) { + for (ic_tns: int32, 0, 16) { + let cse_var_3: int32 = ((co*16) + ci) + res_gem_1[cse_var_3] = (res_gem_1[cse_var_3] + (cast(int32, data_buf_1[(((ic.outer*256) + (ic.inner*16)) + ic_tns)])*cast(int32, weight_buf_1[((((((i1.outer*262144) + (co*16384)) + (ic.outer*4096)) + (ic.inner*256)) + (ci*16)) + ic_tns)]))) + } + } + } + } + } + for (i1_2: int32, 0, 16) { + for (i3_2: int32, 0, 16) { + let cse_var_4: int32 = ((i1_2*16) + i3_2) + res_gem_2: Buffer(res_gem, int32, [256], [])[cse_var_4] = @tir.shift_right(res_gem_1[cse_var_4], 8, dtype=int32) + } + } + for (i1_3: int32, 0, 16) { + for (i3_3: int32, 0, 16) { + let cse_var_5: int32 = ((i1_3*16) + i3_3) + res_gem_3: Buffer(res_gem, int32, [256], [])[cse_var_5] = max(res_gem_2[cse_var_5], 0) + } + } + for (i1_4: int32, 0, 16) { + for (i3_4: int32, 0, 16) { + let cse_var_6: int32 = ((i1_4*16) + i3_4) + res_gem_4: Buffer(res_gem, int32, [256], [])[cse_var_6] = min(res_gem_3[cse_var_6], 127) + } + } + for (i1.inner: int32, 0, 16) { + for (i3_5: int32, 0, 16) { + let cse_var_7: int32 = (i1.inner*16) + res[(((i1.outer*256) + cse_var_7) + i3_5)] = cast(int8, res_gem_4[(cse_var_7 + i3_5)]) + } + } + } + } +} +``` + +### 将拷贝降级到 DMA 传输 + +接下来,将缓冲区范围设置为相应的芯片 VTA SRAM 缓冲区。将加载循环移动到 2D 卷积计算循环中,暂存内存加载,使其适合芯片上 SRAM 缓冲区。最后,用 DMA 拷贝编译指示来注释加载/存储循环外轴,从而在 VTA 上执行大容量内存传输。 + +``` python +# 设置 SRAM 缓冲区的范围 +s[data_buf].set_scope(env.inp_scope) +s[weight_buf].set_scope(env.wgt_scope) +s[res_gemm].set_scope(env.acc_scope) +s[res_shr].set_scope(env.acc_scope) +s[res_min].set_scope(env.acc_scope) +s[res_max].set_scope(env.acc_scope) + +# 块数据和权重缓存读取 +s[data_buf].compute_at(s[res_gemm], ic_out) +s[weight_buf].compute_at(s[res_gemm], ic_out) + +# 用 DMA 拷贝编译指示操作 DRAM->SRAM +s[data_buf].pragma(s[data_buf].op.axis[0], env.dma_copy) +s[weight_buf].pragma(s[weight_buf].op.axis[0], env.dma_copy) + +# 在 SRAM->DRAM 操作上,使用 DMA 拷贝编译指示(这意味着这些拷贝应沿 b_inn 或结果轴 2 执行) +s[res].pragma(s[res].op.axis[2], env.dma_copy) +``` + +### 将计算降级到 VTA 计算内联函数 + +最后一个阶段是降级计算循环到 VTA 硬件内联函数,这是通过将 2D 卷积映射到张量内联函数,并将移位和裁剪计算映射到向量 ALU 实现的。 + +``` python +# 在 batch 张量平铺轴上应用张量化 +s[res_gemm].tensorize(b_tns, env.gemm) + +# 在移位和裁剪操作上添加 ALU 编译指示 +s[res_shr].pragma(s[res_shr].op.axis[0], env.alu) +s[res_min].pragma(s[res_min].op.axis[0], env.alu) +s[res_max].pragma(s[res_max].op.axis[0], env.alu) + +# 将内存负载/存储降级为 DMA 拷贝内联函数,并将计算降级为 VTA 计算内联函数后,查看最终降低的 TVM schedule。 +print(vta.lower(s, [data, weight, res], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(data_1: handle, weight_1: handle, res_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {data: Buffer(data_2: Pointer(int8), int8, [1024], []), + weight: Buffer(weight_2: Pointer(int8), int8, [1048576], []), + res: Buffer(res_2: Pointer(int8), int8, [1024], [])} + buffer_map = {data_1: data, weight_1: weight, res_1: res} + preflattened_buffer_map = {data_1: data_3: Buffer(data_2, int8, [1, 64, 1, 16], []), weight_1: weight_3: Buffer(weight_2, int8, [64, 64, 16, 16], []), res_1: res_3: Buffer(res_2, int8, [1, 64, 1, 16], [])} { + @tir.vta.coproc_dep_push(3, 2, dtype=int32) + for (i1.outer: int32, 0, 4) { + attr [IterVar(vta: int32, (nullptr), "ThreadIndex", "vta")] "coproc_scope" = 2 { + @tir.vta.coproc_dep_pop(3, 2, dtype=int32) + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_uop_scope" = "VTAPushGEMMOp" { + @tir.call_extern("VTAUopLoopBegin", 16, 1, 0, 0, dtype=int32) + @tir.vta.uop_push(0, 1, 0, 0, 0, 0, 0, 0, dtype=int32) + @tir.call_extern("VTAUopLoopEnd", dtype=int32) + } + @tir.vta.coproc_dep_push(2, 1, dtype=int32) + } + for (ic.outer: int32, 0, 4) { + let cse_var_1: int32 = (ic.outer*16) + { + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_scope" = 1 { + @tir.vta.coproc_dep_pop(2, 1, dtype=int32) + @tir.call_extern("VTALoadBuffer2D", @tir.tvm_thread_context(@tir.vta.command_handle(, dtype=handle), dtype=handle), data_2, cse_var_1, 16, 1, 16, 0, 0, 0, 0, 0, 2, dtype=int32) + @tir.call_extern("VTALoadBuffer2D", @tir.tvm_thread_context(@tir.vta.command_handle(, dtype=handle), dtype=handle), weight_2, ((i1.outer*1024) + cse_var_1), 16, 16, 64, 0, 0, 0, 0, 0, 1, dtype=int32) + @tir.vta.coproc_dep_push(1, 2, dtype=int32) + } + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_scope" = 2 { + @tir.vta.coproc_dep_pop(1, 2, dtype=int32) + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_uop_scope" = "VTAPushGEMMOp" { + @tir.call_extern("VTAUopLoopBegin", 16, 1, 0, 16, dtype=int32) + @tir.call_extern("VTAUopLoopBegin", 16, 0, 1, 1, dtype=int32) + @tir.vta.uop_push(0, 0, 0, 0, 0, 0, 0, 0, dtype=int32) + @tir.call_extern("VTAUopLoopEnd", dtype=int32) + @tir.call_extern("VTAUopLoopEnd", dtype=int32) + } + @tir.vta.coproc_dep_push(2, 1, dtype=int32) + } + } + } + @tir.vta.coproc_dep_pop(2, 1, dtype=int32) + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_scope" = 2 { + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_uop_scope" = "VTAPushALUOp" { + @tir.call_extern("VTAUopLoopBegin", 16, 1, 1, 0, dtype=int32) + @tir.vta.uop_push(1, 0, 0, 0, 0, 3, 1, 8, dtype=int32) + @tir.call_extern("VTAUopLoopEnd", dtype=int32) + } + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_uop_scope" = "VTAPushALUOp" { + @tir.call_extern("VTAUopLoopBegin", 16, 1, 1, 0, dtype=int32) + @tir.vta.uop_push(1, 0, 0, 0, 0, 1, 1, 0, dtype=int32) + @tir.call_extern("VTAUopLoopEnd", dtype=int32) + } + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_uop_scope" = "VTAPushALUOp" { + @tir.call_extern("VTAUopLoopBegin", 16, 1, 1, 0, dtype=int32) + @tir.vta.uop_push(1, 0, 0, 0, 0, 0, 1, 127, dtype=int32) + @tir.call_extern("VTAUopLoopEnd", dtype=int32) + } + @tir.vta.coproc_dep_push(2, 3, dtype=int32) + } + attr [IterVar(vta, (nullptr), "ThreadIndex", "vta")] "coproc_scope" = 3 { + @tir.vta.coproc_dep_pop(2, 3, dtype=int32) + for (i1.inner: int32, 0, 16) { + @tir.call_extern("VTAStoreBuffer2D", @tir.tvm_thread_context(@tir.vta.command_handle(, dtype=handle), dtype=handle), i1.inner, 4, res_2, ((i1.outer*16) + i1.inner), 1, 1, 1, dtype=int32) + } + @tir.vta.coproc_dep_push(3, 2, dtype=int32) + } + } + @tir.vta.coproc_sync(, dtype=int32) + @tir.vta.coproc_dep_pop(3, 2, dtype=int32) +} +``` + +## TVM 编译和验证 + +指定 schedule 后,可以将其编译为 TVM 函数。保存模块,然后可以通过 RPC 发送。运行这个函数,并根据 numpy 实现对其进行验证,以确保正确性。 + +``` python +# 编译 TVM 模块 +my_gemm = vta.build( + s, [data, weight, res], tvm.target.Target("ext_dev", host=env.target_host), name="my_gemm" +) +temp = utils.tempdir() +my_gemm.save(temp.relpath("gemm.o")) +remote.upload(temp.relpath("gemm.o")) +f = remote.load_module("gemm.o") + +# 获取远程设备上下文 +ctx = remote.ext_dev(0) + +# 在 (-128, 128] 的 int 范围内随机初始化数据和权重数组 +data_np = np.random.randint(-128, 128, size=(batch_size, in_channels)).astype(data.dtype) +weight_np = np.random.randint(-128, 128, size=(out_channels, in_channels)).astype(weight.dtype) + +# 将数据和权重数组从 2D 打包为 4D 打包布局 +data_packed = data_np.reshape( + batch_size // env.BATCH, env.BATCH, in_channels // env.BLOCK_IN, env.BLOCK_IN +).transpose((0, 2, 1, 3)) +weight_packed = weight_np.reshape( + out_channels // env.BLOCK_OUT, env.BLOCK_OUT, in_channels // env.BLOCK_IN, env.BLOCK_IN +).transpose((0, 2, 1, 3)) + +# 用 tvm.nd.array 将输入/输出数组格式化为 DLPack 标准 +data_nd = tvm.nd.array(data_packed, ctx) +weight_nd = tvm.nd.array(weight_packed, ctx) +res_nd = tvm.nd.array(np.zeros(output_shape).astype(res.dtype), ctx) + +# 清除统计 +if env.TARGET in ["sim", "tsim"]: + simulator.clear_stats() + +# 调用模块进行计算 +f(data_nd, weight_nd, res_nd) + +# 针对 numpy 实现进行验证 +res_ref = np.dot(data_np.astype(env.acc_dtype), weight_np.T.astype(env.acc_dtype)) +res_ref = res_ref >> env.INP_WIDTH +res_ref = np.clip(res_ref, 0, inp_max) +res_ref = res_ref.astype(res.dtype) +res_ref = res_ref.reshape( + batch_size // env.BATCH, env.BATCH, out_channels // env.BLOCK_OUT, env.BLOCK_OUT +).transpose((0, 2, 1, 3)) +np.testing.assert_equal(res_ref, res_nd.numpy()) + +# 打印统计 +if env.TARGET in ["sim", "tsim"]: + sim_stats = simulator.stats() + print("Execution statistics:") + for k, v in sim_stats.items(): + print("\t{:<16}: {:>16}".format(k, v)) + +print("Successful blocked matrix multiply test!") +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:267: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +Execution statistics: + inp_load_nbytes : 4096 + wgt_load_nbytes : 1048576 + acc_load_nbytes : 0 + uop_load_nbytes : 20 + out_store_nbytes: 1024 + gemm_counter : 4096 + alu_counter : 192 +Successful blocked matrix multiply test! +``` + +## 总结 + +本教程演示了 TVM 调度原语如何实现矩阵乘法示例的计算分块,进而能够将任意大的计算映射到有限的硬件加速器资源上。 + +[下载 Python 源代码:matrix_multiply_opt.py](https://tvm.apache.org/docs/_downloads/822e9d945c0bbf1cf23fc4f53c1b7906/matrix_multiply_opt.py) + +[下载 Jupyter Notebook:matrix_multiply_opt.ipynb](https://tvm.apache.org/docs/_downloads/4d3f955a709b320db0d42740fead8ac1/matrix_multiply_opt.ipynb) diff --git a/versioned_docs/version-0.12.0/topic/vta/tutorials/07-autotuning_alu.md b/versioned_docs/version-0.12.0/topic/vta/tutorials/07-autotuning_alu.md new file mode 100644 index 00000000..9a5a429b --- /dev/null +++ b/versioned_docs/version-0.12.0/topic/vta/tutorials/07-autotuning_alu.md @@ -0,0 +1,325 @@ +--- +title: 在 VTA 上自动调优 ALU 融合算子 +--- + +# 在 VTA 上自动调优 ALU 融合算子 + +:::note +单击 [此处](https://tvm.apache.org/docs/topic/vta/tutorials/autotvm/tune_alu_vta.html#sphx-glr-download-topic-vta-tutorials-autotvm-tune-alu-vta-py) 下载完整的示例代码 +::: + +``` python +import os +from mxnet.gluon.model_zoo import vision +import numpy as np +from PIL import Image + +from tvm import topi +import tvm +from tvm import te +from tvm import rpc, autotvm, relay +from tvm.contrib import download +from tvm.autotvm.measure.measure_methods import request_remote +from tvm.autotvm.tuner import XGBTuner, GATuner, RandomTuner, GridSearchTuner +from tvm.autotvm import record + +import vta +from vta.testing import simulator +from vta.top import graph_pack +import copy +``` + +# 编译网络 + +使用来自 Gluon 模型的 Relay 执行特定于 VTA 的编译: + +``` python +def compile_network(env, target, model, start_pack, stop_pack): + # 填充 shape 和数据类型字典 + dtype_dict = {"data": "float32"} + shape_dict = {"data": (env.BATCH, 3, 224, 224)} + + # 下架 gluon 模型,并转换为 Relay + gluon_model = vision.get_model(model, pretrained=True) + mod, params = relay.frontend.from_mxnet(gluon_model, shape_dict) + + # 更新 shape 和类型字典 + shape_dict.update({k: v.shape for k, v in params.items()}) + dtype_dict.update({k: str(v.dtype) for k, v in params.items()}) + + # 在 Relay 中执行量化 + # 注意:我们将 opt_level 设置为 3 以折叠批量规范 + with relay.build_config(opt_level=3): + with relay.quantize.qconfig(global_scale=8.0, skip_conv_layers=[0]): + mod = relay.quantize.quantize(mod, params=params) + + # 对 VTA 目标进行图打包和常量折叠 + if target.device_name == "vta": + assert env.BLOCK_IN == env.BLOCK_OUT + relay_prog = graph_pack( + mod["main"], + env.BATCH, + env.BLOCK_OUT, + env.WGT_WIDTH, + start_name=start_pack, + stop_name=stop_pack, + ) + + return relay_prog, params +``` + +# 设置调优选项 + +调优前,需要应用一些配置。这里以 Pynq-Z1 板为例: + +``` python +# Tracker 主机和端口可以由你的环境设置 +tracker_host = os.environ.get("TVM_TRACKER_HOST", "0.0.0.0") +tracker_port = int(os.environ.get("TVM_TRACKER_PORT", 9190)) + +# 从 vta/config/vta_config.json 文件中加载 VTA 参数 +env = vta.get_env() + +# 此 target 用于交叉编译。可以在你的设备上通过:code:`gcc -v` 来查询。 +# 设置 ``device=arm_cpu`` 在 CPU 上运行推理 +# 或者设置 ``device=vta`` 在 FPGA 上运行推理 +device = "vta" +target = env.target if device == "vta" else env.target_vta_cpu + +# 要编译的 Gluon 模型的名称 +# ``start_pack`` 和 ``stop_pack`` 标签指示在哪里开始和结束图形打包 Relay pass:换言之,从哪里开始和结束转移到 VTA。 +network = "resnet50_v2" +start_pack = "nn.max_pool2d" +stop_pack = "nn.global_avg_pool2d" + +# 调优选项 +log_file = "%s.alu.%s.log" % (device, network) +tuning_option = { + "log_filename": log_file, + "tuner": "random", + "n_trial": 1000, + "early_stopping": None, + "measure_option": autotvm.measure_option( + builder=autotvm.LocalBuilder(n_parallel=1), + runner=autotvm.RPCRunner( + env.TARGET, + host=tracker_host, + port=tracker_port, + number=5, + timeout=60, + # check_correctness=True, # TODO: 当 check_correctness 再次起作用时重新启用。 + ), + ), +} + +def log_to_file(file_out, protocol="json"): + """Log the tuning records into file. + The rows of the log are stored in the format of autotvm.record.encode. + for lhs == rhs, we add an extra rhs = [] record + 将调优日志记录到文件中。 + 日志的行以 autotvm.record.encode 的格式存储。 + 对于 lhs == rhs,添加一个额外的 rhs = [] 来记录 + + Parameters + 参数 + ---------- + file_out : str + The file to log to. + 记录的文件。 + protocol: str, optional + The log protocol. Can be 'json' or 'pickle' + 日志协议。为 'json' 或 ’pickle‘。 + + Returns + 返回值 + ------- + callback : callable + Callback function to do the logging. + 实现日志记录的回调函数。 + """ + + def _callback(_, inputs, results): + with open(file_out, "a") as f: + for inp, result in zip(inputs, results): + f.write(record.encode(inp, result, protocol) + "\n") + + # 只考虑具有相同 lhs 和 rhs 的任务 + if inp.task.args[0] == inp.task.args[1]: + args = list(inp.task.args) + args[1] = (args[0][0], (), args[0][2]) + inp_copy = copy.deepcopy(inp) + inp_copy.task.args = tuple(args) + f.write(record.encode(inp_copy, result, protocol) + "\n") + + return _callback + +def tune_tasks( + tasks, + measure_option, + tuner="xgb", + n_trial=10, + early_stopping=None, + log_filename="tuning.log", + use_transfer_learning=True, +): + + # 创建 tmp 日志文件 + tmp_log_file = log_filename + ".tmp" + if os.path.exists(tmp_log_file): + os.remove(tmp_log_file) + + for i, tsk in enumerate(reversed(tasks)): + prefix = "[Task %2d/%2d] " % (i + 1, len(tasks)) + + # 创建调优器 + if tuner == "xgb" or tuner == "xgb-rank": + tuner_obj = XGBTuner(tsk, loss_type="rank") + elif tuner == "xgb_knob": + tuner_obj = XGBTuner(tsk, loss_type="rank", feature_type="knob") + elif tuner == "ga": + tuner_obj = GATuner(tsk, pop_size=50) + elif tuner == "random": + tuner_obj = RandomTuner(tsk) + elif tuner == "gridsearch": + tuner_obj = GridSearchTuner(tsk) + else: + raise ValueError("Invalid tuner: " + tuner) + + if use_transfer_learning: + if os.path.isfile(tmp_log_file): + tuner_obj.load_history(autotvm.record.load_from_file(tmp_log_file)) + + # 开始调优 + tsk_trial = min(n_trial, len(tsk.config_space)) + tuner_obj.tune( + n_trial=tsk_trial, + early_stopping=early_stopping, + measure_option=measure_option, + callbacks=[ + autotvm.callback.progress_bar(tsk_trial, prefix=prefix), + log_to_file(tmp_log_file), + ], + ) + + # 选择最佳记录到缓存文件 + autotvm.record.pick_best(tmp_log_file, log_filename) + os.remove(tmp_log_file) +``` + +注册特定于 VTA 的调优任务 + +``` python +def register_vta_tuning_tasks(): + from tvm.autotvm.task import TaskExtractEnv + + @tvm.te.tag_scope(tag=topi.tag.ELEMWISE) + def my_clip(x, a_min, a_max): + """Unlike topi's current clip, put min and max into two stages.""" + const_min = tvm.tir.const(a_min, x.dtype) + const_max = tvm.tir.const(a_max, x.dtype) + x = te.compute(x.shape, lambda *i: tvm.te.min(x(*i), const_max), name="clipA") + x = te.compute(x.shape, lambda *i: tvm.te.max(x(*i), const_min), name="clipB") + return x + + # 初始化 autotvm 环境以注册 VTA 算子 + TaskExtractEnv() + + @autotvm.template("add.vta") + def _topi_add(*args, **kwargs): + assert not kwargs, "Do not support kwargs in template function call" + A, B = args[:2] + + with tvm.target.vta(): + res = vta.top.op.add_packed(*args, **kwargs) + res = my_clip(res, 0, 127) + res = topi.cast(res, "int8") + + if tvm.target.Target.current().device_name == "vta": + s = vta.top.op.schedule_add_packed([res]) + else: + s = te.create_schedule([res.op]) + return s, [A, B, res] + + @autotvm.template("multiply.vta") + def _topi_multiply(*args, **kwargs): + assert not kwargs, "Do not support kwargs in template function call" + A, B = args[:2] + + with tvm.target.vta(): + res = vta.top.op.multiply_packed(*args, **kwargs) + res = my_clip(res, 0, 127) + res = topi.cast(res, "int8") + + if tvm.target.Target.current().device_name == "vta": + s = vta.top.op.schedule_multiply_packed([res]) + else: + s = te.create_schedule([res.op]) + return s, [A, B, res] +``` + +最后,启动调优作业,并评估端到端性能。 + +``` python +def tune_and_evaluate(tuning_opt): + + if env.TARGET != "intelfocl": + print("ALU only op only available for intelfocl target") + return + + # 注册 VTA 调优任务 + register_vta_tuning_tasks() + + # 对 Relay 程序进行任务提取 + print("Extract tasks...") + relay_prog, params = compile_network(env, target, network, start_pack, stop_pack) + mod = tvm.IRModule.from_expr(relay_prog) + tasks = autotvm.task.extract_from_program( + mod, + params=params, + ops=( + relay.op.get("add"), + relay.op.get("multiply"), + ), + target=tvm.target.Target(target, host=env.target_host), + ) + + # 过滤掉非打包的 alu 任务 + tasks = list(filter(lambda t: len(t.args[0][1]) > 4, tasks)) + # 过滤掉 float alu 任务 + tasks = list(filter(lambda t: t.args[0][2] != "float32", tasks)) + + # 我们应该已经提取了 10 个卷积任务 + tasks_set = {} + print("Extracted {} alu tasks:".format(len(tasks))) + for tsk in tasks: + print("tsk = ", tsk) + + if len(tsk.args[1][1]) == 0: + args = list(tsk.args) + args[1] = args[0] + tsk.args = tuple(args) + + if (tsk.name, tsk.args) in tasks_set: + print("task {} already exists".format(tsk)) + tasks_set[(tsk.name, tsk.args)] = tsk + + tasks = list(tasks_set.values()) + print("After merged, final #tasks={}, tasks = {}".format(len(tasks), tasks)) + + # 运行调优任务 + print("Tuning...") + tune_tasks(tasks, **tuning_opt) + +# 运行调优并评估结果 +tune_and_evaluate(tuning_option) +``` + +输出结果: + +``` bash +ALU only op only available for intelfocl target +``` + +[下载 Python 源代码:tune_alu_vta.py](https://tvm.apache.org/docs/_downloads/6bbcf342fb9192416b8e1a86a1d4e981/tune_alu_vta.py) + +[下载 Jupyter Notebook:tune_alu_vta.ipynb](https://tvm.apache.org/docs/_downloads/178b6f23dffc01ac92f2cf95f41a5679/tune_alu_vta.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/topic/vta/tutorials/08-autotuning_conv.md b/versioned_docs/version-0.12.0/topic/vta/tutorials/08-autotuning_conv.md new file mode 100644 index 00000000..000c122a --- /dev/null +++ b/versioned_docs/version-0.12.0/topic/vta/tutorials/08-autotuning_conv.md @@ -0,0 +1,479 @@ +--- +title: 在 VTA 上自动调优卷积网络 +--- + +# 在 VTA 上自动调优卷积网络 + +:::note +单击 [此处](https://tvm.apache.org/docs/topic/vta/tutorials/autotvm/tune_relay_vta.html#sphx-glr-download-topic-vta-tutorials-autotvm-tune-relay-vta-py) 下载完整的示例代码 +::: + +**作者**:[Lianmin Zheng](https://github.com/merrymercy), [Thierry Moreau](https://homes.cs.washington.edu/\~moreau/) + +为特定加速器设计自动调优,对于获取任何给定算子的最佳性能至关重要。本教程展示如何在 VTA 上调优整个卷积网络。 + +TVM 中 VTA 的算子实现是用 template 形式编写的。该 template 有许多可调 knobs(平铺因子、虚拟线程等)。对神经网络中的所有卷积算子进行调优。调优后,会生成一个日志文件,存储所有调优算子的最佳 schedule 参数。TVM 编译器编译这些算子时,会查询这个日志文件,从而获得最佳的 knob 参数。 + +## 安装依赖 + +要在 TVM 中使用 autotvm 包,需要安装额外的依赖(如果用的是 Python2,请将「3」更改为「2」): + +``` bash +pip3 install --user psutil xgboost tornado mxnet requests "Pillow<7" cloudpickle +``` + +为了让 TVM 在调优过程中运行更快,推荐使用 Cython 作为 TVM 的 FFI。在 TVM 的根目录下,执行如下命令(若用的是 Python2,将「3」改为「2」): + +``` bash +pip3 install --user cython +sudo make cython3 +``` + +在 Python 代码中导入包: + +``` python +import os +from mxnet.gluon.model_zoo import vision +import numpy as np +from PIL import Image + +from tvm import topi +import tvm +from tvm import te +from tvm import rpc, autotvm, relay +from tvm.contrib import graph_executor, utils, download +from tvm.autotvm.measure.measure_methods import request_remote +from tvm.autotvm.tuner import XGBTuner, GATuner, RandomTuner, GridSearchTuner + +import vta +from vta.testing import simulator +from vta.top import graph_pack +``` + +## 编译网络 + +用来自 Gluon 模型的 Relay 执行特定于 VTA 的编译: + +``` python +def compile_network(env, target, model, start_pack, stop_pack): + # 填充 shape 和数据类型字典 + dtype_dict = {"data": "float32"} + shape_dict = {"data": (env.BATCH, 3, 224, 224)} + + # 下架 gluon 模型,并转换为 Relay + gluon_model = vision.get_model(model, pretrained=True) + mod, params = relay.frontend.from_mxnet(gluon_model, shape_dict) + + # 更新 shape 和类型字典 + shape_dict.update({k: v.shape for k, v in params.items()}) + dtype_dict.update({k: str(v.dtype) for k, v in params.items()}) + + # 在 Relay 中执行量化 + # 注意:将 opt_level 设置为 3,折叠 batch norm + with tvm.transform.PassContext(opt_level=3): + with relay.quantize.qconfig(global_scale=8.0, skip_conv_layers=[0]): + mod = relay.quantize.quantize(mod, params=params) + + # 对 VTA target 进行图打包和常量折叠 + if target.device_name == "vta": + assert env.BLOCK_IN == env.BLOCK_OUT + relay_prog = graph_pack( + mod["main"], + env.BATCH, + env.BLOCK_OUT, + env.WGT_WIDTH, + start_name=start_pack, + stop_name=stop_pack, + ) + + return relay_prog, params +``` + +## 启动 RPC 跟踪器 + +TVM 使用 RPC session 与 Pynq 板进行通信。调优期间,调优器会将生成的代码发送到板上,并测试板上代码的速度。 + +为了扩大调优,TVM 用 RPC 跟踪器来管理多个设备。 RPC 跟踪器是一个集中的控制器节点。可以将所有设备注册到跟踪器。例如,若有 10 个 Pynq 板,可以将它们全部注册到跟踪器,然后并行运行 10 次测试,从而加快调优过程。 + +在主机上运行此命令,启动 RPC 跟踪器。整个调优过程中都需要跟踪器,因此我们需要为这个命令打开一个新终端: + +``` bash +python -m tvm.exec.rpc_tracker --host=0.0.0.0 --port=9190 +``` + +预期输出: + +``` bash +INFO:RPCTracker:bind to 0.0.0.0:9190 +``` + +## 将设备注册到 RPC 跟踪器 + +现在可以将设备注册到跟踪器。第一步是为 Pynq 设备构建 TVM runtime。 + +按照 [VTA:多功能张量加速器](index) 在设备上构建 TVM runtime。然后将设备注册到跟踪器: + +``` bash +python -m tvm.exec.rpc_server --tracker=[HOST_IP]:9190 --key=pynq +``` + +(将 `[HOST_IP]` 替换为你主机的 IP 地址) + +注册设备后,可以通过查询 rpc_tracker 来确认: + +``` bash +python -m tvm.exec.query_rpc_tracker --host=0.0.0.0 --port=9190 +``` + +例如,若有 6 个 Pynq 板和 11 个树莓派 3B,则输出: + +``` bash +Queue Status +---------------------------------- +key total free pending +---------------------------------- +pynq 6 6 0 +rpi3b 11 11 0 +---------------------------------- +``` + +可以将多个设备注册到跟踪器,加速调优。 + +## 设置调优选项 + +调优前,需要应用一些配置。这里以 Pynq-Z1 板为例: + +``` python +# 跟踪器主机和端口可以由你的环境设置 +tracker_host = os.environ.get("TVM_TRACKER_HOST", "127.0.0.1") +tracker_port = int(os.environ.get("TVM_TRACKER_PORT", 9190)) + +# 从 3rdparty/vta-hw/config/vta_config.json 文件加载 VTA 参数 +env = vta.get_env() + +# 此 target 用于交叉编译。可以在你的设备上通过:code:`gcc -v` 来查询它。 +# 设置 ``device=arm_cpu`` 在 CPU 上运行推理 +# 或者设置 ``device=vta`` 在 FPGA 上运行推理 +device = "vta" +target = env.target if device == "vta" else env.target_vta_cpu + +# 要编译的 Gluon 模型的名称 +# ``start_pack`` 和 ``stop_pack`` 标签指示在哪里开始和结束图形打包 Relay pass:换言之,从哪里开始和结束转移到 VTA。 +network = "resnet18_v1" +start_pack = "nn.max_pool2d" +stop_pack = "nn.global_avg_pool2d" + +# 调优选项 +log_file = "%s.%s.log" % (device, network) +tuning_option = { + "log_filename": log_file, + "tuner": "random", + "n_trial": 1000, + "early_stopping": None, + "measure_option": autotvm.measure_option( + builder=autotvm.LocalBuilder(), + runner=autotvm.RPCRunner( + env.TARGET, + host=tracker_host, + port=tracker_port, + number=5, + timeout=60, + module_loader=vta.module_loader(), + # check_correctness=True, # TODO: 当 check_correctness 再次起作用时重新启用。 + ), + ), +} +``` + +:::note +如何设置调优选项 + +通常,此处提供的默认值效果很好。若调优时间充分,可以将 `n_trial`、`early_stopping` 设置为更大的值,使调优运行更长时间。若设备功率不足或 conv2d 算子很大,考虑将超时时间设置大一些。 +::: + +## 开始调优 + +现在可以从网络中提取调优任务并开始调优。这里提供了一个简单的实用函数来调优任务列表。这个函数只是初始版本,它按顺序调整任务列表。未来我们会引入更复杂的调优调度器。 + +由于要在 Pynq FPGA 板上完成调优,请确保 `vta_config.json` 文件中的 `TARGET` 条目设置为 `pynq`。 + +``` python +# 本教程可以跳过此函数的实现。 +def tune_tasks( + tasks, + measure_option, + tuner="xgb", + n_trial=1000, + early_stopping=None, + log_filename="tuning.log", + use_transfer_learning=True, +): + # 创建 tmp 日志文件 + tmp_log_file = log_filename + ".tmp" + if os.path.exists(tmp_log_file): + os.remove(tmp_log_file) + + for i, tsk in enumerate(reversed(tasks)): + prefix = "[Task %2d/%2d] " % (i + 1, len(tasks)) + + # 创建调优器 + if tuner == "xgb" or tuner == "xgb-rank": + tuner_obj = XGBTuner(tsk, loss_type="rank") + elif tuner == "xgb_knob": + tuner_obj = XGBTuner(tsk, loss_type="rank", feature_type="knob") + elif tuner == "ga": + tuner_obj = GATuner(tsk, pop_size=50) + elif tuner == "random": + tuner_obj = RandomTuner(tsk) + elif tuner == "gridsearch": + tuner_obj = GridSearchTuner(tsk) + else: + raise ValueError("Invalid tuner: " + tuner) + + if use_transfer_learning: + if os.path.isfile(tmp_log_file): + tuner_obj.load_history(autotvm.record.load_from_file(tmp_log_file)) + + # 开始调优 + tsk_trial = min(n_trial, len(tsk.config_space)) + tuner_obj.tune( + n_trial=tsk_trial, + early_stopping=early_stopping, + measure_option=measure_option, + callbacks=[ + autotvm.callback.progress_bar(tsk_trial, prefix=prefix), + autotvm.callback.log_to_file(tmp_log_file), + ], + ) + + # 选择最佳记录放到缓存文件 + autotvm.record.pick_best(tmp_log_file, log_filename) + os.remove(tmp_log_file) +``` + +注册特定 VTA 的调优任务 + +``` python +def register_vta_tuning_tasks(): + from tvm.autotvm.task import TaskExtractEnv + + @tvm.te.tag_scope(tag=topi.tag.ELEMWISE) + def my_clip(x, a_min, a_max): + """Unlike topi's current clip, put min and max into two stages.""" + const_min = tvm.tir.const(a_min, x.dtype) + const_max = tvm.tir.const(a_max, x.dtype) + x = te.compute(x.shape, lambda *i: tvm.te.min(x(*i), const_max), name="clipA") + x = te.compute(x.shape, lambda *i: tvm.te.max(x(*i), const_min), name="clipB") + return x + + # 初始化 autotvm 环境并注册 VTA 算子 + TaskExtractEnv() + + @autotvm.template("conv2d_packed.vta") + def _topi_nn_conv2d(*args, **kwargs): + assert not kwargs, "Do not support kwargs in template function call" + A, W = args[:2] + + with tvm.target.vta(): + res = vta.top.conv2d_packed(*args, **kwargs) + res = topi.right_shift(res, 8) + res = my_clip(res, 0, 127) + res = topi.cast(res, "int8") + + if tvm.target.Target.current().device_name == "vta": + s = vta.top.schedule_conv2d_packed([res]) + else: + s = te.create_schedule([res.op]) + return s, [A, W, res] +``` + +最后,启动调优作业,并评估端到端性能。 + +``` python +def tune_and_evaluate(tuning_opt): + # 注册 VTA 调优任务 + register_vta_tuning_tasks() + + # 对 Relay 程序进行任务提取 + print("Extract tasks...") + relay_prog, params = compile_network(env, target, network, start_pack, stop_pack) + mod = tvm.IRModule.from_expr(relay_prog) + tasks = autotvm.task.extract_from_program( + mod, + params=params, + ops=(relay.op.get("nn.conv2d"),), + target=target, + target_host=env.target_host, + ) + + # 过滤掉非打包的 conv2d 任务 + tasks = list(filter(lambda t: len(t.args[0][1]) > 4 and "conv" in t.name, tasks)) + + # 我们应该已经提取了 10 个卷积任务 + assert len(tasks) == 10 + print("Extracted {} conv2d tasks:".format(len(tasks))) + for tsk in tasks: + inp = tsk.args[0][1] + wgt = tsk.args[1][1] + batch = inp[0] * inp[4] + in_filter = inp[1] * inp[5] + out_filter = wgt[0] * wgt[4] + height, width = inp[2], inp[3] + hkernel, wkernel = wgt[2], wgt[3] + hstride, wstride = tsk.args[2][0], tsk.args[2][1] + hpad, wpad = tsk.args[3][0], tsk.args[3][1] + print( + "({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {})".format( + batch, + height, + width, + in_filter, + out_filter, + hkernel, + wkernel, + hpad, + wpad, + hstride, + wstride, + ) + ) + + # 不在网页服务器中运行调优,因为它需要的时间太长。 + # 注释以下行以自行运行。 + return + + # 运行调优任务 + print("Tuning...") + tune_tasks(tasks, **tuning_opt) + + # 评估调优历史 + if env.TARGET != "sim": + # 从队列节点获取远程 + remote = autotvm.measure.request_remote( + env.TARGET, tracker_host, tracker_port, timeout=10000 + ) + # 重新配置 JIT runtime 和 FPGA。 + vta.reconfig_runtime(remote) + vta.program_fpga(remote, bitstream=None) + else: + # 在模拟模式下,本地托管 RPC 服务器。 + remote = rpc.LocalSession() + + # 编译具有历史最佳记录的内核 + with autotvm.tophub.context(target, extra_files=[log_file]): + # 编译网络 + print("Compile...") + if target.device_name != "vta": + with tvm.transform.PassContext(opt_level=3, disabled_pass={"AlterOpLayout"}): + lib = relay.build( + relay_prog, target=target, params=params, target_host=env.target_host + ) + else: + with vta.build_config(opt_level=3, disabled_pass={"AlterOpLayout"}): + lib = relay.build( + relay_prog, target=target, params=params, target_host=env.target_host + ) + + # 导出库 + print("Upload...") + temp = utils.tempdir() + lib.export_library(temp.relpath("graphlib.tar")) + remote.upload(temp.relpath("graphlib.tar")) + lib = remote.load_module("graphlib.tar") + + # 生成图执行器 + ctx = remote.ext_dev(0) if device == "vta" else remote.cpu(0) + m = graph_executor.GraphModule(lib["default"](ctx)) + + # 上传参数到设备 + image = tvm.nd.array((np.random.uniform(size=(1, 3, 224, 224))).astype("float32")) + m.set_input("data", image) + + # 评估 + print("Evaluate inference time cost...") + timer = m.module.time_evaluator("run", ctx, number=1, repeat=10) + tcost = timer() + prof_res = np.array(tcost.results) * 1000 # convert to millisecond + print( + "Mean inference time (std dev): %.2f ms (%.2f ms)" + % (np.mean(prof_res), np.std(prof_res)) + ) + +# 运行调优并评估结果 +tune_and_evaluate(tuning_option) +``` + +输出结果: + +``` bash +Extract tasks... +/workspace/python/tvm/driver/build_module.py:267: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +/workspace/python/tvm/target/target.py:273: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +Extracted 10 conv2d tasks: +(1, 56, 56, 64, 64, 3, 3, 1, 1, 1, 1) +(1, 56, 56, 64, 128, 1, 1, 0, 0, 2, 2) +(1, 56, 56, 64, 128, 3, 3, 1, 1, 2, 2) +(1, 28, 28, 128, 128, 3, 3, 1, 1, 1, 1) +(1, 28, 28, 128, 256, 1, 1, 0, 0, 2, 2) +(1, 28, 28, 128, 256, 3, 3, 1, 1, 2, 2) +(1, 14, 14, 256, 256, 3, 3, 1, 1, 1, 1) +(1, 14, 14, 256, 512, 1, 1, 0, 0, 2, 2) +(1, 14, 14, 256, 512, 3, 3, 1, 1, 2, 2) +(1, 7, 7, 512, 512, 3, 3, 1, 1, 1, 1) +``` + +## 样本输出 + +调优需要编译许多程序,并从中提取特征。所以推荐使用高性能的 CPU。下面给出了一个输出示例。16T CPU 和 6 块 Pynq 板大约需要 2 个小时。 + +``` bash +Extract tasks... +[Warning] Invalid shape during AutoTVM task creation +Extracted 10 conv2d tasks: + Task(func_name=topi_nn_conv2d, args=(('TENSOR', (1, 16, 14, 14, 1, 16), 'int8'), ('TENSOR', (32, 16, 1, 1, 16, 16), 'int8'), (2, 2), (0, 0), (1, 1), 'NCHW1n16c', 'int32'), kwargs={}, workload=('conv2d', (1, 16, 14, 14, 1, 16, 'int8'), (32, 16, 1, 1, 16, 16, 'int8'), (2, 2), (0, 0), (1, 1), 'NCHW1n16c', 'int32')) + Task(func_name=topi_nn_conv2d, args=(('TENSOR', (1, 8, 28, 28, 1, 16), 'int8'), ('TENSOR', (16, 8, 1, 1, 16, 16), 'int8'), (2, 2), (0, 0), (1, 1), 'NCHW1n16c', 'int32'), kwargs={}, workload=('conv2d', (1, 8, 28, 28, 1, 16, 'int8'), (16, 8, 1, 1, 16, 16, 'int8'), (2, 2), (0, 0), (1, 1), 'NCHW1n16c', 'int32')) + Task(func_name=topi_nn_conv2d, args=(('TENSOR', (1, 4, 56, 56, 1, 16), 'int8'), ('TENSOR', (8, 4, 1, 1, 16, 16), 'int8'), (2, 2), (0, 0), (1, 1), 'NCHW1n16c', 'int32'), kwargs={}, workload=('conv2d', (1, 4, 56, 56, 1, 16, 'int8'), (8, 4, 1, 1, 16, 16, 'int8'), (2, 2), (0, 0), (1, 1), 'NCHW1n16c', 'int32')) + Task(func_name=topi_nn_conv2d, args=(('TENSOR', (1, 4, 56, 56, 1, 16), 'int8'), ('TENSOR', (4, 4, 3, 3, 16, 16), 'int8'), (1, 1), (1, 1), (1, 1), 'NCHW1n16c', 'int32'), kwargs={}, workload=('conv2d', (1, 4, 56, 56, 1, 16, 'int8'), (4, 4, 3, 3, 16, 16, 'int8'), (1, 1), (1, 1), (1, 1), 'NCHW1n16c', 'int32')) + Task(func_name=topi_nn_conv2d, args=(('TENSOR', (1, 8, 28, 28, 1, 16), 'int8'), ('TENSOR', (8, 8, 3, 3, 16, 16), 'int8'), (1, 1), (1, 1), (1, 1), 'NCHW1n16c', 'int32'), kwargs={}, workload=('conv2d', (1, 8, 28, 28, 1, 16, 'int8'), (8, 8, 3, 3, 16, 16, 'int8'), (1, 1), (1, 1), (1, 1), 'NCHW1n16c', 'int32')) + Task(func_name=topi_nn_conv2d, args=(('TENSOR', (1, 4, 56, 56, 1, 16), 'int8'), ('TENSOR', (8, 4, 3, 3, 16, 16), 'int8'), (2, 2), (1, 1), (1, 1), 'NCHW1n16c', 'int32'), kwargs={}, workload=('conv2d', (1, 4, 56, 56, 1, 16, 'int8'), (8, 4, 3, 3, 16, 16, 'int8'), (2, 2), (1, 1), (1, 1), 'NCHW1n16c', 'int32')) + Task(func_name=topi_nn_conv2d, args=(('TENSOR', (1, 16, 14, 14, 1, 16), 'int8'), ('TENSOR', (16, 16, 3, 3, 16, 16), 'int8'), (1, 1), (1, 1), (1, 1), 'NCHW1n16c', 'int32'), kwargs={}, workload=('conv2d', (1, 16, 14, 14, 1, 16, 'int8'), (16, 16, 3, 3, 16, 16, 'int8'), (1, 1), (1, 1), (1, 1), 'NCHW1n16c', 'int32')) + Task(func_name=topi_nn_conv2d, args=(('TENSOR', (1, 8, 28, 28, 1, 16), 'int8'), ('TENSOR', (16, 8, 3, 3, 16, 16), 'int8'), (2, 2), (1, 1), (1, 1), 'NCHW1n16c', 'int32'), kwargs={}, workload=('conv2d', (1, 8, 28, 28, 1, 16, 'int8'), (16, 8, 3, 3, 16, 16, 'int8'), (2, 2), (1, 1), (1, 1), 'NCHW1n16c', 'int32')) + Task(func_name=topi_nn_conv2d, args=(('TENSOR', (1, 32, 7, 7, 1, 16), 'int8'), ('TENSOR', (32, 32, 3, 3, 16, 16), 'int8'), (1, 1), (1, 1), (1, 1), 'NCHW1n16c', 'int32'), kwargs={}, workload=('conv2d', (1, 32, 7, 7, 1, 16, 'int8'), (32, 32, 3, 3, 16, 16, 'int8'), (1, 1), (1, 1), (1, 1), 'NCHW1n16c', 'int32')) + Task(func_name=topi_nn_conv2d, args=(('TENSOR', (1, 16, 14, 14, 1, 16), 'int8'), ('TENSOR', (32, 16, 3, 3, 16, 16), 'int8'), (2, 2), (1, 1), (1, 1), 'NCHW1n16c', 'int32'), kwargs={}, workload=('conv2d', (1, 16, 14, 14, 1, 16, 'int8'), (32, 16, 3, 3, 16, 16, 'int8'), (2, 2), (1, 1), (1, 1), 'NCHW1n16c', 'int32')) +Tuning... +[Task 1/10] Current/Best: 0.72/ 23.24 GFLOPS | Progress: (480/1000) | 640.31 s Done. +[Task 2/10] Current/Best: 0.00/ 27.69 GFLOPS | Progress: (576/1000) | 810.09 s Done. +[Task 3/10] Current/Best: 0.00/ 22.97 GFLOPS | Progress: (1000/1000) | 1125.37 s Done. +[Task 4/10] Current/Best: 0.00/ 31.26 GFLOPS | Progress: (1000/1000) | 1025.52 s Done. +[Task 5/10] Current/Best: 0.00/ 15.15 GFLOPS | Progress: (1000/1000) | 1236.58 s Done. +[Task 6/10] Current/Best: 0.00/ 22.74 GFLOPS | Progress: (1000/1000) | 906.60 s Done. +[Task 7/10] Current/Best: 0.00/ 15.27 GFLOPS | Progress: (1000/1000) | 1056.25 s Done. +[Task 8/10] Current/Best: 0.00/ 2.18 GFLOPS | Progress: (1000/1000) | 2275.29 s Done. +[Task 9/10] Current/Best: 2.23/ 3.99 GFLOPS | Progress: (1000/1000) | 2527.25 s Done. +[Task 10/10] Current/Best: 1.56/ 6.32 GFLOPS | Progress: (480/1000) | 1304.84 s Done. +Compile... +Upload... +Evaluate inference time cost... +Mean inference time (std dev): 621.79 ms (0.14 ms) +``` + +:::note +**遇到困难?** + +自动调优模块容易出错。若总是看到「0.00/ 0.00 GFLOPS」,那么肯定有问题。 + +首先,确保设置了正确的设备配置。然后,通过在脚本开头添加以下代码来打印调试信息。它将打印每个测试结果,可以在其中找到有用的报错消息。 + +``` python +import logging +logging.getLogger('autotvm').setLevel(logging.DEBUG) +``` + +最后,随时在 https://discuss.tvm.apache.org 上向社区寻求帮助。 +::: + +[下载 Python 源代码:tune_relay_vta.py](https://tvm.apache.org/docs/_downloads/d7b7e50e9f5b4ff04d55a56e52314c71/tune_relay_vta.py) + +[下载 Jupyter Notebook:tune_relay_vta.ipynb](https://tvm.apache.org/docs/_downloads/b1b0cbd807166348a0eabbad6bfbbdaf/tune_relay_vta.ipynb) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/topic/vta/tutorials/_category_.json b/versioned_docs/version-0.12.0/topic/vta/tutorials/_category_.json new file mode 100644 index 00000000..f7b59bf8 --- /dev/null +++ b/versioned_docs/version-0.12.0/topic/vta/tutorials/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 300 +} diff --git a/versioned_docs/version-0.12.0/topic/vta/tutorials/index.md b/versioned_docs/version-0.12.0/topic/vta/tutorials/index.md new file mode 100644 index 00000000..b657a8b1 --- /dev/null +++ b/versioned_docs/version-0.12.0/topic/vta/tutorials/index.md @@ -0,0 +1,24 @@ +--- +title: VTA 教程 +--- +# VTA 教程 + +本节包含有关 VTA 以及如何用 TVM/Relay 来定位 VTA 的教程。 + +* [简单矩阵乘法](mat_mul) +* [VTA 入门](start_vta) + +## 编译深度学习模型 + +* [在 VTA 上部署来自 MxNet 的预训练视觉模型](deploy_mxnet) +* [在 VTA 上部署来自 Darknet 的预训练视觉检测模型](deploy_darknet) + +## 优化张量算子 + +* [2D 卷积优化](conv_opt) +* [矩阵乘法 Blocking](mat_mul_blocking) + +## 自动调优 + +* [在 VTA 上自动调优 ALU 融合算子](autotuning_alu) +* [在 VTA 上自动调优卷积网络](autotuning_conv) \ No newline at end of file diff --git a/versioned_docs/version-0.12.0/topic/vta_idx.md b/versioned_docs/version-0.12.0/topic/vta_idx.md new file mode 100644 index 00000000..30435859 --- /dev/null +++ b/versioned_docs/version-0.12.0/topic/vta_idx.md @@ -0,0 +1,29 @@ +--- +title: VTA:多功能张量加速器 +slug: /topic/vta +--- + +# VTA:多功能张量加速器 + +多功能张量加速器(Versatile Tensor Accelerator,简称 VTA)是一个开放、通用和可定制的深度学习加速器,具有完整的基于 TVM 的编译器堆栈。VTA 揭示了主流深度学习加速器最显著的共同特征。TVM 和 VTA 形成了一个端到端的硬件-软件深度学习系统堆栈,其中包括硬件设计、驱动程序、JIT runtime 和基于 TVM 的优化编译器堆栈。 + +![图片](/img/docs/uwsampl/web-data/main/vta/blogpost/vta_overview.png) + +VTA 的主要功能: + +* 通用、模块化、开源硬件。 +* 简化了部署到 FPGA 的工作流程。 +* 模拟器支持常规工作站上的原型编译传递。 +* 基于 Pynq 的驱动程序和 JIT runtime,用于模拟和 FPGA 硬件后端。 +* 端到端 TVM 堆栈集成。 + +本节包含与 VTA 相关的所有资源的链接: + +* [VTA 安装指南](vta/install) +* [VTA 设计和开发指南](vta/dev) +* [VTA 教程](vta/tutorials) + +## 文献 + +* 阅读 VTA [博客文章](https://tvm.apache.org/2018/07/12/vta-release-announcement)。 +* 阅读 VTA 技术报告:[用于深度学习的开放硬件软件堆栈](https://arxiv.org/abs/1807.04188)。 diff --git a/versioned_docs/version-0.12.0/tutorial/01-intro.md b/versioned_docs/version-0.12.0/tutorial/01-intro.md new file mode 100644 index 00000000..8606e69b --- /dev/null +++ b/versioned_docs/version-0.12.0/tutorial/01-intro.md @@ -0,0 +1,61 @@ +--- +title: TVM 原理介绍 +--- + +:::note +单击 [此处](https://tvm.apache.org/docs/tutorial/introduction.html#sphx-glr-download-tutorial-introduction-py) 下载完整的示例代码 +::: + +# 介绍 + +**作者**:[Jocelyn Shiue](https://github.com/CircleSpin),[Chris Hoge](https://github.com/hogepodge),[Lianmin Zheng](https://github.com/merrymercy) + +Apache TVM 是一个用于 CPU、GPU 和机器学习加速器的开源机器学习编译器框架,旨在让机器学习工程师能够在任何硬件后端上高效地优化和运行计算。本教程的目的是通过定义和演示关键概念,来引导用户了解 TVM 的所有主要功能。新用户完整学完本教程后应该对 TVM 架构及其工作原理有了基本的了解,从而能够用 TVM 来自动优化模型。 + +## 内容 +1. TVM 原理简介 +2. 安装 TVM +3. 使用命令行界面编译和优化模型 +4. 使用 Python 接口编译和优化模型 +5. 使用张量表达式操作算子 +6. 使用模板和 AutoTVM 优化算子 +7. 使用无模板的 AutoScheduler 优化算子 +8. 交叉编译和远程过程调用(RPC) +9. 用 GPU 编译深度学习模型 + +# TVM 和模型优化概述 +下图说明了使用 TVM 优化编译器框架转换时所采取的步骤。 + +![A High Level View of TVM](/img/docs/apache/tvm-site/main/images/tutorial/overview.png) + +1. 从 TensorFlow、PyTorch 或 ONNX 等框架导入模型。在导入阶段中,TVM 可以从其他框架(如 TensorFlow、PyTorch 或 ONNX)中提取模型。 TVM 为前端提供的支持水平会随着我们不断改进这个开源项目而变化。如果在将模型导入 TVM 时遇到问题,可以将其转换为 ONNX。 + +2. 翻译成 TVM 的高级模型语言 Relay。已导入 TVM 的模型在 Relay 中表示。Relay 是神经网络的功能语言和中间表示(IR)。它支持: + * 传统的数据流式表示 + * 函数式作用域,let-binding 使其成为一种功能齐全的可微语言 + * 允许用户混用两种编程风格的能力 + + Relay 应用图级优化 pass 来优化模型。 + +3. 降级为张量表达式(TE)表示。降级是指将较高级的表示转换为较低级的表示。应用了高级优化之后,Relay 通过运行 FuseOps pass,把模型划分为许多小的子图,并将子图降级为 TE 表示。张量表达式(TE)是一种用于描述张量计算的领域特定语言。 TE 还提供了几个 schedule 原语来指定底层循环优化,例如循环切分、矢量化、并行化、循环展开和融合。为将 Relay 表示转换为 TE 表示,TVM 包含了一个张量算子清单(TOPI),其中包含常用张量算子的预定义模板(例如,conv2d、transpose)。 + +4. 使用 auto-tuning 模块 AutoTVM 或 AutoScheduler 搜索最佳 schedule。schedule 为 TE 中定义的算子或子图指定底层循环优化。auto-tuning 模块搜索最佳 schedule,并将其与 cost model 和设备上的测量值进行比较。 TVM 中有两个 auto-tuning 模块。 + * AutoTVM:基于模板的 auto-tuning 模块。它运行搜索算法以在用户定义的模板中找到可调 knob 的最佳值。 TOPI 中已经提供了常用算子的模板。 + * AutoScheduler(又名 Ansor):无模板的 auto-tuning 模块。它不需要预定义的 schedule 模板,而是通过分析计算定义自动生成搜索空间,然后在生成的搜索空间中搜索最佳 schedule。 + +5. 为模型编译选择最佳配置。调优后,auto-tuning 模块会生成 JSON 格式的调优记录。此步骤为每个子图选择最佳 schedule。 + +6. 降级为张量中间表示(TIR,TVM 的底层中间表示)。基于调优步骤选择最佳配置后,所有 TE 子图降级为 TIR 并通过底层优化 pass 进行优化。接下来,优化的 TIR 降级为硬件平台的目标编译器。这是生成可部署到生产的优化模型的最终代码生成阶段。 TVM 支持多种不同的编译器后端: + + * LLVM,针对任意微处理器架构,包括标准 x86 和 ARM 处理器、AMDGPU 和 NVPTX 代码生成,以及 LLVM 支持的任何其他平台。 + * 特定编译器,例如 NVCC(NVIDIA 的编译器)。 + * 嵌入式和特定 target,通过 TVM 的 自定义代码生成(Bring Your Own Codegen, BYOC)框架实现。 + +7. 编译成机器码。compiler-specific 的生成代码最终可降级为机器码。 + TVM 可将模型编译为可链接对象模块,然后轻量级 TVM runtime 可以用 C 语言的 API 来动态加载模型,也可以为 Python 和 Rust 等其他语言提供入口点。或将 runtime 和模型放在同一个 package 里时,TVM 可以对其构建捆绑部署。 + +本教程的其余部分将更详细地介绍 TVM 的这些方面: + +[下载 Python 源代码:introduction.py](https://tvm.apache.org/docs/_downloads/31d82e25454740f5ba711497485c0dd4/introduction.py) + +[下载 Jupyter Notebook:introduction.ipynb](https://tvm.apache.org/docs/_downloads/9f81bc348ac4107d0670f512b8943a99/introduction.ipynb) diff --git a/versioned_docs/version-0.12.0/tutorial/02-install.md b/versioned_docs/version-0.12.0/tutorial/02-install.md new file mode 100644 index 00000000..f5393ef3 --- /dev/null +++ b/versioned_docs/version-0.12.0/tutorial/02-install.md @@ -0,0 +1,23 @@ +--- +title: 安装 TVM +--- + +# 安装 TVM +**作者**:[Jocelyn Shiue](https://github.com/CircleSpin),[Chris Hoge](https://github.com/hogepodge) + +根据需要和工作环境,安装 TVM 的方法有如下几种: + +* 从源码安装 +* 从第三方二进制包安装。 + +## 从源码安装 + +推荐从源码安装 TVM 。这个方法可以启用特定功能,例如 GPU 支持、微控制器支持(microTVM)、调试 runtime 和其他功能。想积极为 TVM 项目做贡献的用户更推荐此方法。查看 从源码安装 TVM 以获取完整的教程。 + +## 从二进制包安装 + +用第三方二进制分发包来安装更快捷方便。 TLCPack 是一个从 TVM 源码构建二进制包的第三方志愿者社区。它提供了一个包含具有不同特性的平台上的安装说明的支持矩阵。查看 TLCPack 以了解更多信息。注意,第三方二进制包可能包含与其捆绑的硬件驱动程序的附加许可条款。 + +[下载 Python 源代码: install.py](https://tvm.apache.org/docs/_downloads/067cf39a44d9f315a39f8a7547c556d8/install.py) + +[下载 Jupyter Notebook: install.ipynb](https://tvm.apache.org/docs/_downloads/2a4c6a9cfa43e8afef159a2bf1b99108/install.ipynb) diff --git a/versioned_docs/version-0.12.0/tutorial/03-compile.md b/versioned_docs/version-0.12.0/tutorial/03-compile.md new file mode 100644 index 00000000..d2a75e6f --- /dev/null +++ b/versioned_docs/version-0.12.0/tutorial/03-compile.md @@ -0,0 +1,347 @@ +--- +title: 使用 TVMC 编译和优化模型 +--- + +# 使用 TVMC 编译和优化模型 +**作者**:[Leandro Nunes](https://github.com/leandron),[Matthew Barrett](https://github.com/mbaret),[Chris Hoge](https://github.com/hogepodge) + +本节将介绍 TVMC(TVM 的命令行驱动程序)。TVMC 通过命令行界面执行 TVM 功能(包括对模型的自动调优、编译、分析和执行)。 + +学完本节后,可用 TVMC 实现下面的任务: + +* 为 TVM runtime 编译预训练的 ResNet-50 v2 模型。 +* 用编译好的模型预测真实图像,并解释输出和模型性能。 +* 使用 TVM 在 CPU上调优模型。 +* 用 TVM 收集的调优数据,重新编译优化过的模型。 +* 通过优化的模型预测图像,并比较输出和模型性能。 + +本节对 TVM 及 TVMC 的功能进行了概述,并为了解 TVM 的工作原理奠定基础。 + +## 使用 TVMC + +TVMC 是 Python 应用程序,也是 TVM Python 软件包的一部分。用 Python 包安装 TVM 时,会得到一个叫 ```tvmc``` 的命令行应用程序。平台和安装方法不同,此命令的位置也会发生变化。 + +另外,如果 ```$PYTHONPATH``` 上有 TVM 这个 Python 模块,则可通过可执行 Python 模块(用 ```python -m tvm.driver.tvmc``` 命令)来访问命令行驱动功能。 + +本教程用 ```tvmc ``` 或 ```python -m tvm.driver.tvmc ``` 来打开 TVMC 命令行。 + +使用如下命令查看帮助页: + +``` bash +tvmc --help +``` + +```tvmc``` 可用的 TVM 的主要功能来自子命令 ```compile```、```run``` 和 ```tune```。使用 ```tvmc --help``` 查看给定子命令的特定选项。本教程将介绍这些命令,开始前请先下载一个预训练的模型。 + +## 获取模型 + +在本教程中,我们将使用 ResNet-50 v2。ResNet-50 是一个用来对图像进行分类的 50 层深的卷积神经网络。接下来要用的模型,已经在超过100万张具有1000种不同分类的图像上,进行了预训练。该网络的输入图像的大小为224x224。推荐下载 [Netron](https://netron.app/)(免费的 ML 模型查看器)来更深入地探索 ResNet-50 模型的组织结构。 + +本教程使用 ONNX 格式的模型: +``` bash +wget https://github.com/onnx/models/raw/b9a54e89508f101a1611cd64f4ef56b9cb62c7cf/vision/classification/resnet/model/resnet50-v2-7.onnx +``` +:::note 支持的模型格式: +TVMC 支持用 Keras、ONNX、TensorFlow、TFLite 和 Torch 创建的模型。可用 ```--model-format``` 选项指明正在使用的模型格式。执行 ```tvmc compile --help``` 来获取更多信息。 +::: + +:::note 向 TVM 添加对 ONNX 的支持 +TVM 依赖系统中可用的 ONNX Python 库。用命令 ```pip3 install --user onnx onnxoptimizer``` 来安装 ONNX。如果具有 root 访问权限并且希望全局安装 ONNX,则可以删除 ```--user``` 选项。```onnxoptimizer``` 依赖是可选的,仅用于 ```onnx>=1.9```。 +::: + +## 将 ONNX 模型编译到 TVM Runtime + +下载 ResNet-50 模型后,用 ```tvmc compile``` 对其进行编译。编译的输出结果是模型(被编译为目标平台的动态库)的 TAR 包。用 TVM runtime 可在目标设备上运行该模型: + +``` bash +# 大概需要几分钟,取决于设备 +tvmc compile \ +--target "llvm" \ +--input-shapes "data:[1,3,224,224]" \ +--output resnet50-v2-7-tvm.tar \ +resnet50-v2-7.onnx +``` + +查看 ```tvmc compile``` 在模块中创建的文件: + +``` bash +mkdir model +tar -xvf resnet50-v2-7-tvm.tar -C model +ls model +``` + +解压后有三个文件: + +* ```mod.so``` 是可被 TVM runtime 加载的模型,表示为 C++ 库。 +* ```mod.json``` 是 TVM Relay 计算图的文本表示。 +* ```mod.params``` 是包含预训练模型参数的文件。 + +模块可由应用程序直接加载,而模型可通过 TVM runtime API 运行。 + +:::note 定义正确的 target +指定正确的 target(选项 ```--target```)可大大提升编译模块的性能,因为可利用 target 上可用的硬件功能。参阅 针对 x86 CPU 自动调优卷积网络 获取更多信息。建议确定好使用的 CPU 型号以及可选功能,然后适当地设置 target。 +::: + +## 使用 TVMC 运行来自编译模块的模型 + +将模型编译到模块后,可用 TVM runtime 对其进行预测。 TVMC 具有内置的 TVM runtime,允许运行已编译的 TVM 模型。要用 TVMC 运行模型并预测,需要: + +* 刚生成的编译模块。 +* 用来预测的模型的有效输入。 + +模型的张量 shape、格式和数据类型各不相同。因此,大多数模型都需要预处理和后处理,确保输入有效,并能够解释输出。 TVMC 采用了 NumPy 的 ```.npz``` 格式的输入和输出,可很好地支持将多个数组序列化到一个文件中。 + +本教程中的图像输入使用的是一张猫的图像,你也可以根据喜好选择其他图像。 + +![Cat](https://s3.amazonaws.com/model-server/inputs/kitten.jpg) + +### 输入预处理 + +ResNet-50 v2 模型的输入应该是 ImageNet 格式。下面是 ResNet-50 v2 预处理图像的脚本示例。 + +首先用 ```pip3 install --user pillow``` 下载 Python 图像库,以满足脚本运行对图像库的依赖。 + +``` python +#!python ./preprocess.py +from tvm.contrib.download import download_testdata +from PIL import Image +import numpy as np + +img_url = "https://s3.amazonaws.com/model-server/inputs/kitten.jpg" +img_path = download_testdata(img_url, "imagenet_cat.png", module="data") + +# 重设大小为 224x224 +resized_image = Image.open(img_path).resize((224, 224)) +img_data = np.asarray(resized_image).astype("float32") + +# ONNX 需要 NCHW 输入, 因此对数组进行转换 +img_data = np.transpose(img_data, (2, 0, 1)) + +# 根据 ImageNet 进行标准化 +imagenet_mean = np.array([0.485, 0.456, 0.406]) +imagenet_stddev = np.array([0.229, 0.224, 0.225]) +norm_img_data = np.zeros(img_data.shape).astype("float32") +for i in range(img_data.shape[0]): + norm_img_data[i, :, :] = (img_data[i, :, :] / 255 - imagenet_mean[i]) / imagenet_stddev[i] + +# 添加 batch 维度 +img_data = np.expand_dims(norm_img_data, axis=0) + +# 保存为 .npz(输出 imagenet_cat.npz) +np.savez("imagenet_cat", data=img_data) +``` + +### 运行编译模块 + +有了模型和输入数据,接下来运行 TVMC 进行预测: + +``` bash +tvmc run \ +--inputs imagenet_cat.npz \ +--output predictions.npz \ +resnet50-v2-7-tvm.tar +``` + +```.tar``` 模型文件中包括一个 C++ 库、对 Relay 模型的描述文件,以及模型的参数文件。 TVMC 包括 TVM runtime(可加载模型,并对输入进行预测)。运行以上命令,TVMC 会输出一个新文件 ```predictions.npz```,其中包含 NumPy 格式的模型输出张量。 + +在此示例中,用于编译模型的和运行模型的是同一台机器。某些情况下,可能会用 RPC Tracker 来远程运行它。查看 ```tvmc run --help``` 来了解有关这些选项的更多信息。 + +### 输出后处理 + +如前所述,每个模型提供输出张量的方式都不一样。 + +本示例中,我们需要用专为该模型提供的查找表,运行一些后处理(post-processing),从而使得 ResNet-50 v2 的输出形式更具有可读性。 + +下面的脚本是一个后处理示例,它从编译模块的输出中提取标签: + +``` python +#!python ./postprocess.py +import os.path +import numpy as np + +from scipy.special import softmax + +from tvm.contrib.download import download_testdata + +# 下载标签列表 +labels_url = "https://s3.amazonaws.com/onnx-model-zoo/synset.txt" +labels_path = download_testdata(labels_url, "synset.txt", module="data") + +with open(labels_path, "r") as f: + labels = [l.rstrip() for l in f] + +output_file = "predictions.npz" + +# 打开并读入输出张量 +if os.path.exists(output_file): + with np.load(output_file) as data: + scores = softmax(data["output_0"]) + scores = np.squeeze(scores) + ranks = np.argsort(scores)[::-1] + + for rank in ranks[0:5]: + print("class='%s' with probability=%f" % (labels[rank], scores[rank])) +``` + +这个脚本的运行输出如下: + +``` bash +python postprocess.py +# class='n02123045 tabby, tabby cat' with probability=0.610553 +# class='n02123159 tiger cat' with probability=0.367179 +# class='n02124075 Egyptian cat' with probability=0.019365 +# class='n02129604 tiger, Panthera tigris' with probability=0.001273 +# class='n04040759 radiator' with probability=0.000261 +``` + +用其他图像替换上述猫的图像,看看 ResNet 模型做了什么样的预测。 + +## 自动调优 ResNet 模型 + +以前的模型被编译到 TVM runtime 上运行,因此不包含特定于平台的优化。本节将介绍如何用 TVMC,针对工作平台构建优化模型。 + +用编译的模块推理,有时可能无法获得预期的性能。在这种情况下,可用自动调优器更好地配置模型,从而提高性能。 TVM 中的调优是指,在给定 target 上优化模型,使其运行得更快。与训练或微调不同,它不会影响模型的准确性,而只会影响 runtime 性能。作为调优过程的一部分,TVM 实现并运行许多不同算子的变体,以查看哪个性能最佳。这些运行的结果存储在调优记录文件(tune 命令的最终输出)中。 + +调优最少要包含: + +* 运行此模型的目标设备的平台要求 +* 存储调优记录的输出文件的路径 +* 要调优的模型的路径。 + +下面的示例演示了其工作流程: + +``` bash +# 默认搜索算法需要 xgboost,有关调优搜索算法的详细信息,参见下文 +pip install xgboost + +tvmc tune \ +--target "llvm" \ +--output resnet50-v2-7-autotuner_records.json \ +resnet50-v2-7.onnx +``` + +此例中,为 ```--target``` 标志指定更具体的 target 时,会得到更好的结果。例如,在 Intel i7 处理器上,可用 ```--target llvm -mcpu=skylake```。这个调优示例把 LLVM 作为指定架构的编译器,在 CPU 上进行本地调优。 + +TVMC 针对模型的参数空间进行搜索,为算子尝试不同的配置,然后选择平台上运行最快的配置。虽然这是基于 CPU 和模型操作的引导式搜索,但仍需要几个小时才能完成搜索。搜索的输出将保存到 ```resnet50-v2-7-autotuner_records.json``` 文件中,该文件之后会用于编译优化模型。 + +:::note 定义调优搜索算法 +这个搜索算法默认用 ```XGBoost Grid``` 算法进行引导。根据模型复杂度和可用时间,可选择不同的算法。完整列表可查看 ```tvmc tune --help```。 +::: + +对于消费级的 Skylake CPU,输出如下: + +``` bash +tvmc tune \ +--target "llvm -mcpu=broadwell" \ +--output resnet50-v2-7-autotuner_records.json \ +resnet50-v2-7.onnx +# [Task 1/24] Current/Best: 9.65/ 23.16 GFLOPS | Progress: (60/1000) | 130.74 s Done. +# [Task 1/24] Current/Best: 3.56/ 23.16 GFLOPS | Progress: (192/1000) | 381.32 s Done. +# [Task 2/24] Current/Best: 13.13/ 58.61 GFLOPS | Progress: (960/1000) | 1190.59 s Done. +# [Task 3/24] Current/Best: 31.93/ 59.52 GFLOPS | Progress: (800/1000) | 727.85 s Done. +# [Task 4/24] Current/Best: 16.42/ 57.80 GFLOPS | Progress: (960/1000) | 559.74 s Done. +# [Task 5/24] Current/Best: 12.42/ 57.92 GFLOPS | Progress: (800/1000) | 766.63 s Done. +# [Task 6/24] Current/Best: 20.66/ 59.25 GFLOPS | Progress: (1000/1000) | 673.61 s Done. +# [Task 7/24] Current/Best: 15.48/ 59.60 GFLOPS | Progress: (1000/1000) | 953.04 s Done. +# [Task 8/24] Current/Best: 31.97/ 59.33 GFLOPS | Progress: (972/1000) | 559.57 s Done. +# [Task 9/24] Current/Best: 34.14/ 60.09 GFLOPS | Progress: (1000/1000) | 479.32 s Done. +# [Task 10/24] Current/Best: 12.53/ 58.97 GFLOPS | Progress: (972/1000) | 642.34 s Done. +# [Task 11/24] Current/Best: 30.94/ 58.47 GFLOPS | Progress: (1000/1000) | 648.26 s Done. +# [Task 12/24] Current/Best: 23.66/ 58.63 GFLOPS | Progress: (1000/1000) | 851.59 s Done. +# [Task 13/24] Current/Best: 25.44/ 59.76 GFLOPS | Progress: (1000/1000) | 534.58 s Done. +# [Task 14/24] Current/Best: 26.83/ 58.51 GFLOPS | Progress: (1000/1000) | 491.67 s Done. +# [Task 15/24] Current/Best: 33.64/ 58.55 GFLOPS | Progress: (1000/1000) | 529.85 s Done. +# [Task 16/24] Current/Best: 14.93/ 57.94 GFLOPS | Progress: (1000/1000) | 645.55 s Done. +# [Task 17/24] Current/Best: 28.70/ 58.19 GFLOPS | Progress: (1000/1000) | 756.88 s Done. +# [Task 18/24] Current/Best: 19.01/ 60.43 GFLOPS | Progress: (980/1000) | 514.69 s Done. +# [Task 19/24] Current/Best: 14.61/ 57.30 GFLOPS | Progress: (1000/1000) | 614.44 s Done. +# [Task 20/24] Current/Best: 10.47/ 57.68 GFLOPS | Progress: (980/1000) | 479.80 s Done. +# [Task 21/24] Current/Best: 34.37/ 58.28 GFLOPS | Progress: (308/1000) | 225.37 s Done. +# [Task 22/24] Current/Best: 15.75/ 57.71 GFLOPS | Progress: (1000/1000) | 1024.05 s Done. +# [Task 23/24] Current/Best: 23.23/ 58.92 GFLOPS | Progress: (1000/1000) | 999.34 s Done. +# [Task 24/24] Current/Best: 17.27/ 55.25 GFLOPS | Progress: (1000/1000) | 1428.74 s Done. +``` + +调优 session 需要很长时间,因此 ```tvmc tune``` 提供了许多选项来自定义调优过程,包括重复次数(例如 ```--repeat``` 和 ```--number```)、要用的调优算法等。查看 ```tvmc tune --help``` 了解更多信息。 + +## 使用调优数据编译优化模型 + +从上述调优过程的输出文件 ```resnet50-v2-7-autotuner_records.json`` 可获取调优记录。该文件可用来: + +* 作为进一步调优的输入(通过 ```tvmc tune --tuning-records```) +* 作为编译器的输入 + +执行 ```tvmc compile --tuning-records``` 命令让编译器利用这个结果为指定 target 上的模型生成高性能代码。查看 ```tvmc compile --help``` 来获取更多信息。 + +模型的调优数据收集到后,可用优化的算子重新编译模型来加快计算速度。 + +``` bash +tvmc compile \ +--target "llvm" \ +--tuning-records resnet50-v2-7-autotuner_records.json \ +--output resnet50-v2-7-tvm_autotuned.tar \ +resnet50-v2-7.onnx +``` + +验证优化模型是否运行并产生相同结果: + +``` bash +tvmc run \ +--inputs imagenet_cat.npz \ +--output predictions.npz \ +resnet50-v2-7-tvm_autotuned.tar + +python postprocess.py +``` + +验证预测值是否相同: + +``` bash +# class='n02123045 tabby, tabby cat' with probability=0.610550 +# class='n02123159 tiger cat' with probability=0.367181 +# class='n02124075 Egyptian cat' with probability=0.019365 +# class='n02129604 tiger, Panthera tigris' with probability=0.001273 +# class='n04040759 radiator' with probability=0.000261 +``` + +## 比较调优和未调优的模型 + +TVMC 提供了模型之间的基本性能评估工具。可指定重复次数,也可指定 TVMC 报告模型的运行时间(独立于 runtime 启动)。可大致了解调优对模型性能的提升程度。例如,对 Intel i7 系统进行测试时,调优后的模型比未调优的模型运行速度快 47%: + +``` bash +tvmc run \ +--inputs imagenet_cat.npz \ +--output predictions.npz \ +--print-time \ +--repeat 100 \ +resnet50-v2-7-tvm_autotuned.tar + +# Execution time summary: +# mean (ms) max (ms) min (ms) std (ms) +# 92.19 115.73 89.85 3.15 + +tvmc run \ +--inputs imagenet_cat.npz \ +--output predictions.npz \ +--print-time \ +--repeat 100 \ +resnet50-v2-7-tvm.tar + +# Execution time summary: +# mean (ms) max (ms) min (ms) std (ms) +# 193.32 219.97 185.04 7.11 +``` + +## 写在最后 + +本教程介绍了 TVMC( TVM 的命令行驱动程序),演示了如何编译、运行和调优模型,还讨论了对输入和输出进行预处理和后处理的必要性。调优后,演示如何比较未优化和优化模型的性能。 + +本文档展示了一个在本地使用 ResNet-50 v2 的简单示例。然而,TVMC 支持更多功能,包括交叉编译、远程执行和分析/基准测试。 + +用 ```tvmc --help``` 命令查看其他可用选项。 + +下个教程 ```Compiling and Optimizing a Model with the Python Interface``` 将介绍用 Python 接口的相同编译和优化步骤。 + +[下载 Python 源代码:tvmc_command_line_driver.py](https://tvm.apache.org/docs/_downloads/233ceda3a682ae5df93b4ce0bcfbf870/tvmc_command_line_driver.py) + +[下载 Jupyter Notebook:tvmc_command_line_driver.ipynb](https://tvm.apache.org/docs/_downloads/efe0b02e219b28e0bd85fbdda35ba8ac/tvmc_command_line_driver.ipynb) diff --git a/versioned_docs/version-0.12.0/tutorial/04-tvmc_python.md b/versioned_docs/version-0.12.0/tutorial/04-tvmc_python.md new file mode 100644 index 00000000..3af2bfb9 --- /dev/null +++ b/versioned_docs/version-0.12.0/tutorial/04-tvmc_python.md @@ -0,0 +1,206 @@ +--- +title: 使用 TVMC Python 入门:TVM 的高级 API +--- + +:::note +注意:单击 [此处](https://tvm.apache.org/docs/tutorial/tvmc_python.html#sphx-glr-download-tutorial-tvmc-python-py) 下载完整的示例代码 +::: + +**作者**:[Jocelyn Shiue](https://github.com/CircleSpin) + +本节将介绍针对 TVM 初学者设计的脚本工具。 + +开始前如果没有下载示例模型,需要先通过终端下载 resnet 模型: + +``` bash +mkdir myscripts +cd myscripts +wget https://github.com/onnx/models/raw/b9a54e89508f101a1611cd64f4ef56b9cb62c7cf/vision/classification/resnet/model/resnet50-v2-7.onnx +mv resnet50-v2-7.onnx my_model.onnx +touch tvmcpythonintro.py +``` + +用你熟悉的文本编辑器来编辑 Python 文件。 + +## 第 0 步:导入 + +``` python +from tvm.driver import tvmc +``` + +## 第 1 步:加载模型 + +将模型导入 TVMC。这一步将机器学习模型从支持的框架,转换为 TVM 的高级图形表示语言 —— Relay。这是为 TVM 中的所有模型统一起点。目前支持的框架:Keras、ONNX、TensorFlow、TFLite 和 PyTorch。 + +``` python +model = tvmc.load('my_model.onnx') # 第 1 步:加载 +``` + +查看 Relay,可运行 `model.summary()`。 + +所有框架都支持用 shape_dict 参数覆盖输入 shape。对于大多数框架,这是可选的;但对 PyTorch 是必需的,因为 TVM 无法自动搜索它。 + +``` python +model = tvmc.load('my_model.onnx', shape_dict={'input1' : [1, 2, 3, 4], 'input2' : [1, 2, 3, 4]}) #第一步: 加载 + shape_dict +``` + +推荐通过 [netron](https://netron.app/) 查看模型 input/shape_dict。打开模型后,单击第一个节点查看输入部分中的 name 和 shape。 + +## 第 2 步:编译 + +模型现在是用 Relay 表示的,下一步是将其编译到要运行的硬件(称为 target)。这个编译过程将模型从 Relay,翻译成目标机器可理解的底层语言。 + +编译模型需要一个 tvm.target 字符串。查看 [文档](https://tvm.apache.org/docs/api/python/target.html) 了解有关 tvm.targets 及其选项的更多信息。一些例子如下: + +1. cuda (英伟达 GPU) +2. llvm (CPU) +3. llvm -mcpu=cascadelake(英特尔 CPU) + +``` python +package = tvmc.compile(model, target="llvm") # 第 2 步:编译 +``` + +编译完成后返回一个 package。 + +## 第 3 步:运行 + +编译后的 package 可在目标硬件上运行。设备输入选项有:CPU、Cuda、CL、Metal 和 Vulkan。 + +``` python +result = tvmc.run(package, device="cpu") # 第 3 步:运行 +``` + +用 `print(result)` 打印结果。 + +## 第 1.5 步:调优(可选并推荐) + +通过调优可进一步提高运行速度。此可选步骤用机器学习来查看模型(函数)中的每个操作,并找到一种更快的方法来运行它。这一步通过 cost 模型,以及对可能的 schedule 进行基准化来实现。 + +这里的 target 与编译过程用到的 target 是相同的。 + +``` python +tvmc.tune(model, target="llvm") # 第 1.5 步:可选 Tune +``` + +终端输出如下所示: + +``` bash +[Task 1/13] Current/Best: 82.00/ 106.29 GFLOPS | Progress: (48/769) | 18.56 s +[Task 1/13] Current/Best: 54.47/ 113.50 GFLOPS | Progress: (240/769) | 85.36 s +..... +``` + +出现的 UserWarnings 可忽略。调优会使最终结果运行更快,但调优过程会耗费几个小时的时间。 + +参阅下面的“保存调优结果”部分,若要应用结果,务必将调优结果传给编译。 + +``` python +tvmc.compile(model, target="llvm", tuning_records = "records.log") # 第 2 步:编译 +``` + +## 保存并在终端中启动进程 + +``` bash +python my_tvmc_script.py +``` + +## 示例结果 + +``` bash +Time elapsed for training: 18.99 s +Execution time summary: +mean (ms) max (ms) min (ms) std (ms) + 25.24 26.12 24.89 0.38 + + + +Output Names: +['output_0'] +``` + +## TVMC 附加功能 + +## 保存模型 + +加载模型(第 1 步)后可保存 Relay 版本来提高之后的工作效率。模型将被储存在你指定的位置,随后可以被转换过的语法使用。 + +``` python +model = tvmc.load('my_model.onnx') # 第 1 步:加载 +model.save(desired_model_path) +``` + +## 保存 package + +模型编译完成(第 2 步)后,可将 package 保存下来。 + +``` python +tvmc.compile(model, target="llvm", package_path="whatever") # 第 2 步:编译 + +new_package = tvmc.TVMCPackage(package_path="whatever") +result = tvmc.run(new_package, device="cpu") # 第 3 步:运行 +``` + +## 使用 Autoscheduler + +使用下一代 TVM,运行速度会更快。schedule 的搜索空间以前是手写的,而现在是自动生成的。 (了解更多: [1](https://tvm.apache.org/2021/03/03/intro-auto-scheduler),[2](https://arxiv.org/abs/2006.06762)) + +``` python +tvmc.tune(model, target="llvm", enable_autoscheduler = True) +``` + +## 保存调优结果 + +把调优结果保存在文件中,方便以后复用。 + +* 方法 1: + ``` python + log_file = "hello.json" + + # 运行 tuning + tvmc.tune(model, target="llvm", tuning_records=log_file) + + ... + + # 运行 tuning,然后复用 tuning 的结果 + tvmc.tune(model, target="llvm",prior_records=log_file) + ``` + +* 方法 2: + ``` python + # 运行 tuning + tuning_records = tvmc.tune(model, target="llvm") + + ... + + # 运行 tuning,然后复用 tuning 的结果 + tvmc.tune(model, target="llvm",prior_records=tuning_records) + ``` + +## 对更复杂的模型调优 + +如果 T 的打印类似 `.........T.T..T..T..T.T.T.T.T.T.`,则增加搜索时间范围: + +``` python +tvmc.tune(model, trials=10000, timeout=10,) +``` + +## 为远程设备编译模型 + +为不在本地计算机上的硬件进行编译时,TVMC 支持使用远程过程调用(RPC)。要设置 RPC 服务器,可参考本 [文档](https://tvm.apache.org/docs/tutorials/get_started/cross_compilation_and_rpc.html) 中的“在设备上设置 RPC 服务器”部分。 + +TVMC 脚本包括以下内容,并进行了相应调整: + +``` python +tvmc.tune( + model, + target=target, # 编译 target 为字符串 // 要编译的设备 + target_host=target_host, # 主机处理器 + hostname=host_ip_address, # 远程基准测试时使用的 RPC 跟踪器的 IP 地址 + port=port_number, # 要连接的 RPC 跟踪器的端口。默认为 9090。 + rpc_key=your_key, # 目标设备的 RPC 跟踪器密钥。提供 rpc_tracker 时需要 +) +``` + +[`下载 Python 源代码:tvmc_python.py`](https://tvm.apache.org/docs/_downloads/10724e9ad9c29faa223c1d5eab6dbef9/tvmc_python.py) + +[`下载 Jupyter Notebook:tvmc_python.ipynb`](https://tvm.apache.org/docs/_downloads/8d55b8f991fb704002f768367ce2d1a2/tvmc_python.ipynb) diff --git a/versioned_docs/version-0.12.0/tutorial/05-python_AutoTVM.md b/versioned_docs/version-0.12.0/tutorial/05-python_AutoTVM.md new file mode 100644 index 00000000..1e3c3165 --- /dev/null +++ b/versioned_docs/version-0.12.0/tutorial/05-python_AutoTVM.md @@ -0,0 +1,599 @@ +--- +title: 使用 Python 接口(AutoTVM)编译和优化模型 +--- + +# 使用 Python 接口(AutoTVM)编译和优化模型 + +:::note +单击 [此处](https://tvm.apache.org/docs/tutorial/autotvm_relay_x86.html#sphx-glr-download-tutorial-autotvm-relay-x86-py) 下载完整的示例代码 +::: + +**作者**:[Chris Hoge](https://github.com/hogepodge) + +[TVMC 教程](https://tvm.apache.org/docs/tutorial/tvmc_command_line_driver) 介绍了如何用 TVM 的命令行界面(TVMC)编译、运行和调优预训练的模型 ResNet-50 v2。TVM 不仅是一个命令行工具,也是一个具有多种不同语言的 API 优化框架,极大方便了机器学习模型的使用。 + +本节内容将介绍与使用 TVMC 相同的基础知识,不同的是这节内容是用 Python API 来实现的。完成本节后学习后,我们将用 TVM 的 Python API 实现以下任务: + +* 为 TVM runtime 编译预训练的 ResNet-50 v2 模型。 +* 用编译的模型预测真实图像,并解释输出和模型性能。 +* 用 TVM 对 CPU 上建模的模型进行调优。 +* 用 TVM 收集的调优数据重新编译优化模型。 +* 用优化模型预测图像,并比较输出和模型性能。 + +本节目标是概述 TVM 的功能,以及如何通过 Python API 使用它们。 + +TVM 是一个深度学习编译器框架,有许多不同的模块可用于处理深度学习模型和算子。本教程将介绍如何用 Python API 加载、编译和优化模型。 + +首先导入一些依赖,包括用于加载和转换模型的 `onnx`、用于下载测试数据的辅助实用程序、用于处理图像数据的 Python 图像库、用于图像数据预处理和后处理的 `numpy`、TVM Relay 框架和 TVM 图形处理器。 + +``` python +import onnx +from tvm.contrib.download import download_testdata +from PIL import Image +import numpy as np +import tvm.relay as relay +import tvm +from tvm.contrib import graph_executor +``` + +## 下载和加载 ONNX 模型 + +本教程中,我们会用到 ResNet-50 v2。ResNet-50 是一个深度为 50 层的卷积神经网络,适用于图像分类任务。我们即将用到的模型已经在超过 100 万张、具有 1000 种不同分类的图像上进行了预训练。该神经网络的输入图像大小为 224x224。推荐下载 [Netron](https://netron.app/)(免费的 ML 模型查看器 )了解更多 ResNet-50 模型的结构信息。 + +TVM 提供帮助库来下载预训练模型。通过提供模型 URL、文件名和模型类型,TVM 可下载模型并将其保存到磁盘。可用 ONNX runtime 将 ONNX 模型实例加载到内存。 + +:::note 使用其他模型格式 +TVM 支持许多流行的模型格式。可在 TVM 文档的 [编译深度学习模型](https://tvm.apache.org/docs/how_to/compile_models/index.html#tutorial-frontend) 部分找到支持的列表。 +::: + +``` python +model_url = ( + "https://github.com/onnx/models/raw/main/" + "vision/classification/resnet/model/" + "resnet50-v2-7.onnx" +) + +model_path = download_testdata(model_url, "resnet50-v2-7.onnx", module="onnx") +onnx_model = onnx.load(model_path) + +# 为 numpy 的 RNG 设置 seed,得到一致的结果 +np.random.seed(0) +``` + +## 下载、预处理和加载测试图像 + +模型的张量 shape、格式和数据类型各不相同。因此,大多数模型都需要一些预处理和后处理,以确保输入有效,并能解释输出。 TVMC 采用了 NumPy 的 `.npz` 格式的输入和输出数据。 + +本教程中的图像输入使用的是一张猫的图像,你也可以根据喜好选择其他图像。 + + ![https://s3.amazonaws.com/model-server/inputs/kitten.jpg](https://s3.amazonaws.com/model-server/inputs/kitten.jpg) + +下载图像数据,然后将其转换为 numpy 数组作为模型的输入。 + +``` python +img_url = "https://s3.amazonaws.com/model-server/inputs/kitten.jpg" +img_path = download_testdata(img_url, "imagenet_cat.png", module="data") + +# 重设大小为 224x224 +resized_image = Image.open(img_path).resize((224, 224)) +img_data = np.asarray(resized_image).astype("float32") + +# 输入图像是 HWC 布局,而 ONNX 需要 CHW 输入,所以转换数组 +img_data = np.transpose(img_data, (2, 0, 1)) + +# 根据 ImageNet 输入规范进行归一化 +imagenet_mean = np.array([0.485, 0.456, 0.406]).reshape((3, 1, 1)) +imagenet_stddev = np.array([0.229, 0.224, 0.225]).reshape((3, 1, 1)) +norm_img_data = (img_data / 255 - imagenet_mean) / imagenet_stddev + +# 添加 batch 维度,期望 4 维输入:NCHW。 +img_data = np.expand_dims(norm_img_data, axis=0) +``` + +## 使用 Relay 编译模型 + +下一步是编译 ResNet 模型,首先用 *from_onnx* 导入器,将模型导入到 Relay 中。然后,用标准优化,将模型构建到 TVM 库中,最后从库中创建一个 TVM 计算图 runtime 模块。 + +``` python +target = "llvm" +``` + +:::note 定义正确的 target +指定正确的 target(选项 `--target`)可大大提升编译模块的性能,因为可利用 target 上可用的硬件功能。参阅 [针对 x86 CPU 自动调优卷积网络](https://tvm.apache.org/docs/how_to/tune_with_autotvm/tune_relay_x86.html#tune-relay-x86) 获取更多信息。建议确定好使用的 CPU 型号以及可选功能,然后适当地设置 target。例如,对于某些处理器,可用 `target = "llvm -mcpu=skylake"`;对于具有 AVX-512 向量指令集的处理器,可用 `target = "llvm -mcpu=skylake-avx512"`。 +::: + +``` python +# 输入名称可能因模型类型而异 +# 可用 Netron 工具检查输入名称 +input_name = "data" +shape_dict = {input_name: img_data.shape} + +mod, params = relay.frontend.from_onnx(onnx_model, shape_dict) + +with tvm.transform.PassContext(opt_level=3): + lib = relay.build(mod, target=target, params=params) + +dev = tvm.device(str(target), 0) +module = graph_executor.GraphModule(lib["default"](dev)) +``` + +输出结果: + +``` plain +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +## 在 TVM Runtime 执行 + +编译好模型后,就可用 TVM runtime 对其进行预测。要用 TVM 运行模型并进行预测,需要: + +* 刚生成的编译模型。 +* 用来预测的模型的有效输入。 + +``` python +dtype = "float32" +module.set_input(input_name, img_data) +module.run() +output_shape = (1, 1000) +tvm_output = module.get_output(0, tvm.nd.empty(output_shape)).numpy() +``` + +## 收集基本性能数据 + +收集与未优化模型相关的基本性能数据,然后将其与调优后的模型进行比较。为了解释 CPU 噪声,在多个 batch 中多次重复计算,然后收集关于均值、中值和标准差的基础统计数据。 + +``` python +import timeit + +timing_number = 10 +timing_repeat = 10 +unoptimized = ( + np.array(timeit.Timer(lambda: module.run()).repeat(repeat=timing_repeat, number=timing_number)) + * 1000 + / timing_number +) +unoptimized = { + "mean": np.mean(unoptimized), + "median": np.median(unoptimized), + "std": np.std(unoptimized), +} + +print(unoptimized) +``` + +输出结果: + +``` plain +{'mean': 495.13895513002353, 'median': 494.6680843500417, 'std': 1.3081147373726523} +``` + +## 输出后处理 + +如前所述,每个模型提供输出张量的方式都不一样。 + +本示例中,我们需要用专为该模型提供的查找表,运行一些后处理(post-processing),从而使得 ResNet-50 v2 的输出形式更具有可读性。 + +``` python +from scipy.special import softmax + +# 下载标签列表 +labels_url = "https://s3.amazonaws.com/onnx-model-zoo/synset.txt" +labels_path = download_testdata(labels_url, "synset.txt", module="data") + +with open(labels_path, "r") as f: + labels = [l.rstrip() for l in f] + +# 打开输出文件并读取输出张量 +scores = softmax(tvm_output) +scores = np.squeeze(scores) +ranks = np.argsort(scores)[::-1] +for rank in ranks[0:5]: + print("class='%s' with probability=%f" % (labels[rank], scores[rank])) +``` + +输出结果: + +``` plain +class='n02123045 tabby, tabby cat' with probability=0.621103 +class='n02123159 tiger cat' with probability=0.356379 +class='n02124075 Egyptian cat' with probability=0.019712 +class='n02129604 tiger, Panthera tigris' with probability=0.001215 +class='n04040759 radiator' with probability=0.000262 +``` + +预期输出如下: + +``` plain +# class='n02123045 tabby, tabby cat' with probability=0.610553 +# class='n02123159 tiger cat' with probability=0.367179 +# class='n02124075 Egyptian cat' with probability=0.019365 +# class='n02129604 tiger, Panthera tigris' with probability=0.001273 +# class='n04040759 radiator' with probability=0.000261 +``` + +## 调优模型 + +以前的模型被编译到 TVM runtime 上运行,因此不包含特定于平台的优化。本节将介绍如何用 TVMC,针对工作平台构建优化模型。 + +用编译的模块推理,有时可能无法获得预期的性能。在这种情况下,可用自动调优器更好地配置模型,从而提高性能。 TVM 中的调优是指,在给定 target 上优化模型,使其运行得更快。与训练或微调不同,它不会影响模型的准确性,而只会影响 runtime 性能。作为调优过程的一部分,TVM 实现并运行许多不同算子的变体,以查看哪个性能最佳。这些运行的结果存储在调优记录文件中。 + +最简单的形式中,调优需要: + +* 运行此模型的设备的规格 +* 存储调优记录的输出文件的路径 +* 要调优的模型的路径。 + +``` python +import tvm.auto_scheduler as auto_scheduler +from tvm.autotvm.tuner import XGBTuner +from tvm import autotvm +``` + +设置部分基本参数,运行由一组特定参数生成的编译代码并测试其性能。`number` 指定将要测试的不同配置的数量,而 `repeat` 指定将对每个配置进行多少次测试。 `min_repeat_ms` 指定运行配置测试需要多长时间,如果重复次数低于此时间,则增加其值,在 GPU 上进行精确调优时此选项是必需的,在 CPU 调优则不是必需的,将此值设置为 0表示禁用,`timeout` 指明每个测试配置运行训练代码的时间上限。 + +``` python +number = 10 +repeat = 1 +min_repeat_ms = 0 # 调优 CPU 时设置为 0 +timeout = 10 # 秒 + +# 创建 TVM 运行器 +runner = autotvm.LocalRunner( + number=number, + repeat=repeat, + timeout=timeout, + min_repeat_ms=min_repeat_ms, + enable_cpu_cache_flush=True, +) +``` + +创建简单结构来保存调优选项。使用 XGBoost 算法来指导搜索。如果要在投产的项目中应用,则需要将试验次数设置为大于此处的 20。对于 CPU 推荐 1500,对于 GPU 推荐 3000-4000。所需的试验次数可能取决于特定的模型和处理器,要找到调优时间和模型优化之间的最佳平衡,得花一些时间评估一系列值的性能。 + +运行调优需要大量时间,所以这里将试验次数设置为 10,但不推荐使用这么小的值。`early_stopping` 参数是使得搜索提前停止的试验最小值。measure option 决定了构建试用代码并运行的位置,本示例用的是刚创建的 `LocalRunner` 和 `LocalBuilder`。`Tuning_records` 选项指定将调优数据写入的哪个文件中。 + +``` python +tuning_option = { + "tuner": "xgb", + "trials": 20, + "early_stopping": 100, + "measure_option": autotvm.measure_option( + builder=autotvm.LocalBuilder(build_func="default"), runner=runner + ), + "tuning_records": "resnet-50-v2-autotuning.json", +} +``` + +:::note 定义调优搜索算法 +此搜索默认情况下使用 XGBoost Grid 算法进行引导。根据模型复杂性和可用时长可选择不同的算法。 +::: + +:::note 设置调优参数 +为节省时间将试验次数和提前停止次数设置为 20 和 100,数值设置越大,性能越好,所需时间也越长。收敛所需的试验次数根据模型和目标平台的不同而变化。 +::: + +``` python +# 首先从 onnx 模型中提取任务 +tasks = autotvm.task.extract_from_program(mod["main"], target=target, params=params) + +# 按顺序调优提取的任务 +for i, task in enumerate(tasks): + prefix = "[Task %2d/%2d] " % (i + 1, len(tasks)) + tuner_obj = XGBTuner(task, loss_type="rank") + tuner_obj.tune( + n_trial=min(tuning_option["trials"], len(task.config_space)), + early_stopping=tuning_option["early_stopping"], + measure_option=tuning_option["measure_option"], + callbacks=[ + autotvm.callback.progress_bar(tuning_option["trials"], prefix=prefix), + autotvm.callback.log_to_file(tuning_option["tuning_records"]), + ], + ) +``` + +输出结果: + +``` plain +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " + +[Task 1/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 1/25] Current/Best: 17.48/ 17.48 GFLOPS | Progress: (4/20) | 6.23 s +[Task 1/25] Current/Best: 6.16/ 17.48 GFLOPS | Progress: (8/20) | 9.24 s +[Task 1/25] Current/Best: 11.54/ 22.69 GFLOPS | Progress: (12/20) | 11.75 s +[Task 1/25] Current/Best: 16.79/ 22.69 GFLOPS | Progress: (16/20) | 13.44 s +[Task 1/25] Current/Best: 11.62/ 23.90 GFLOPS | Progress: (20/20) | 15.17 s Done. + +[Task 2/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 2/25] Current/Best: 12.28/ 12.93 GFLOPS | Progress: (4/20) | 3.93 s +[Task 2/25] Current/Best: 14.28/ 17.39 GFLOPS | Progress: (8/20) | 5.27 s +[Task 2/25] Current/Best: 20.53/ 20.53 GFLOPS | Progress: (12/20) | 6.61 s +[Task 2/25] Current/Best: 11.88/ 20.53 GFLOPS | Progress: (16/20) | 7.87 s +[Task 2/25] Current/Best: 19.41/ 20.53 GFLOPS | Progress: (20/20) | 9.47 s Done. + +[Task 3/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 3/25] Current/Best: 1.64/ 10.51 GFLOPS | Progress: (4/20) | 5.83 s +[Task 3/25] Current/Best: 15.62/ 17.07 GFLOPS | Progress: (8/20) | 7.73 s +[Task 3/25] Current/Best: 15.07/ 17.07 GFLOPS | Progress: (12/20) | 9.42 s +[Task 3/25] Current/Best: 7.27/ 24.05 GFLOPS | Progress: (16/20) | 11.36 s +[Task 3/25] Current/Best: 12.61/ 24.05 GFLOPS | Progress: (20/20) | 15.89 s Done. + +[Task 4/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 4/25] Current/Best: 9.66/ 20.74 GFLOPS | Progress: (4/20) | 2.38 s +[Task 4/25] Current/Best: 6.87/ 20.74 GFLOPS | Progress: (8/20) | 7.09 s +[Task 4/25] Current/Best: 21.79/ 21.79 GFLOPS | Progress: (12/20) | 11.94 s +[Task 4/25] Current/Best: 17.19/ 21.79 GFLOPS | Progress: (16/20) | 14.31 s +[Task 4/25] Current/Best: 13.38/ 21.79 GFLOPS | Progress: (20/20) | 16.40 s Done. + +[Task 5/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 5/25] Current/Best: 9.68/ 10.34 GFLOPS | Progress: (4/20) | 2.58 s +[Task 5/25] Current/Best: 11.96/ 13.13 GFLOPS | Progress: (8/20) | 4.62 s +[Task 5/25] Current/Best: 11.49/ 18.26 GFLOPS | Progress: (12/20) | 7.64 s +[Task 5/25] Current/Best: 11.79/ 22.76 GFLOPS | Progress: (16/20) | 9.05 s +[Task 5/25] Current/Best: 11.98/ 22.76 GFLOPS | Progress: (20/20) | 10.95 s Done. + +[Task 6/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 6/25] Current/Best: 12.28/ 20.75 GFLOPS | Progress: (4/20) | 4.08 s +[Task 6/25] Current/Best: 19.20/ 20.75 GFLOPS | Progress: (8/20) | 5.83 s +[Task 6/25] Current/Best: 13.29/ 20.75 GFLOPS | Progress: (12/20) | 7.77 s +[Task 6/25] Current/Best: 20.29/ 20.75 GFLOPS | Progress: (16/20) | 10.00 s +[Task 6/25] Current/Best: 3.74/ 20.75 GFLOPS | Progress: (20/20) | 12.53 s Done. + +[Task 7/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 7/25] Current/Best: 11.29/ 12.56 GFLOPS | Progress: (4/20) | 3.61 s +[Task 7/25] Current/Best: 20.39/ 21.29 GFLOPS | Progress: (8/20) | 5.11 s +[Task 7/25] Current/Best: 16.12/ 21.29 GFLOPS | Progress: (12/20) | 7.01 s +[Task 7/25] Current/Best: 12.36/ 21.29 GFLOPS | Progress: (16/20) | 9.05 s +[Task 7/25] Current/Best: 6.38/ 21.95 GFLOPS | Progress: (20/20) | 11.49 s Done. + +[Task 8/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 8/25] Current/Best: 10.24/ 14.44 GFLOPS | Progress: (4/20) | 2.91 s +[Task 8/25] Current/Best: 9.52/ 14.44 GFLOPS | Progress: (8/20) | 8.03 s +[Task 8/25] Current/Best: 12.94/ 14.44 GFLOPS | Progress: (12/20) | 14.49 s +[Task 8/25] Current/Best: 18.92/ 18.92 GFLOPS | Progress: (16/20) | 16.58 s +[Task 8/25] Current/Best: 20.19/ 20.19 GFLOPS | Progress: (20/20) | 23.57 s Done. + +[Task 9/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 9/25] Current/Best: 14.38/ 15.82 GFLOPS | Progress: (4/20) | 11.96 s +[Task 9/25] Current/Best: 23.36/ 23.36 GFLOPS | Progress: (8/20) | 13.71 s +[Task 9/25] Current/Best: 8.34/ 23.36 GFLOPS | Progress: (12/20) | 16.27 s +[Task 9/25] Current/Best: 18.17/ 23.36 GFLOPS | Progress: (16/20) | 19.12 s +[Task 9/25] Current/Best: 9.22/ 23.36 GFLOPS | Progress: (20/20) | 27.69 s +[Task 10/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 10/25] Current/Best: 18.38/ 18.38 GFLOPS | Progress: (4/20) | 2.56 s +[Task 10/25] Current/Best: 15.58/ 18.38 GFLOPS | Progress: (8/20) | 4.18 s +[Task 10/25] Current/Best: 13.30/ 19.16 GFLOPS | Progress: (12/20) | 5.71 s +[Task 10/25] Current/Best: 19.30/ 20.58 GFLOPS | Progress: (16/20) | 6.82 s +[Task 10/25] Current/Best: 8.86/ 20.58 GFLOPS | Progress: (20/20) | 8.33 s Done. + +[Task 11/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 11/25] Current/Best: 11.06/ 18.40 GFLOPS | Progress: (4/20) | 3.40 s +[Task 11/25] Current/Best: 17.14/ 18.40 GFLOPS | Progress: (8/20) | 6.19 s +[Task 11/25] Current/Best: 16.38/ 18.40 GFLOPS | Progress: (12/20) | 8.26 s +[Task 11/25] Current/Best: 13.59/ 21.40 GFLOPS | Progress: (16/20) | 11.11 s +[Task 11/25] Current/Best: 19.64/ 21.83 GFLOPS | Progress: (20/20) | 13.19 s Done. + +[Task 12/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 12/25] Current/Best: 7.87/ 18.27 GFLOPS | Progress: (4/20) | 5.68 s +[Task 12/25] Current/Best: 5.29/ 18.27 GFLOPS | Progress: (8/20) | 9.61 s +[Task 12/25] Current/Best: 18.95/ 19.15 GFLOPS | Progress: (12/20) | 11.57 s +[Task 12/25] Current/Best: 15.07/ 19.15 GFLOPS | Progress: (16/20) | 14.51 s +[Task 12/25] Current/Best: 15.33/ 19.15 GFLOPS | Progress: (20/20) | 16.46 s Done. + +[Task 13/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 13/25] Current/Best: 8.75/ 17.46 GFLOPS | Progress: (4/20) | 3.75 s +[Task 13/25] Current/Best: 15.84/ 20.93 GFLOPS | Progress: (8/20) | 6.37 s +[Task 13/25] Current/Best: 19.65/ 21.81 GFLOPS | Progress: (12/20) | 9.48 s +[Task 13/25] Current/Best: 12.32/ 21.81 GFLOPS | Progress: (16/20) | 12.86 s +[Task 13/25] Current/Best: 18.88/ 21.81 GFLOPS | Progress: (20/20) | 15.15 s Done. + +[Task 14/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 14/25] Current/Best: 13.62/ 13.62 GFLOPS | Progress: (4/20) | 3.39 s +[Task 14/25] Current/Best: 6.18/ 13.62 GFLOPS | Progress: (8/20) | 5.59 s +[Task 14/25] Current/Best: 20.88/ 20.88 GFLOPS | Progress: (12/20) | 8.29 s +[Task 14/25] Current/Best: 16.63/ 20.88 GFLOPS | Progress: (16/20) | 9.99 s Done. + +[Task 14/25] Current/Best: 17.36/ 20.88 GFLOPS | Progress: (20/20) | 11.79 s +[Task 15/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 15/25] Current/Best: 16.39/ 17.82 GFLOPS | Progress: (4/20) | 2.72 s +[Task 15/25] Current/Best: 14.58/ 18.33 GFLOPS | Progress: (8/20) | 4.05 s +[Task 15/25] Current/Best: 10.47/ 22.24 GFLOPS | Progress: (12/20) | 6.26 s +[Task 15/25] Current/Best: 20.60/ 22.24 GFLOPS | Progress: (16/20) | 9.86 s +[Task 15/25] Current/Best: 9.80/ 22.24 GFLOPS | Progress: (20/20) | 10.87 s +[Task 16/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 16/25] Current/Best: 20.68/ 20.68 GFLOPS | Progress: (4/20) | 3.01 s +[Task 16/25] Current/Best: 3.07/ 20.68 GFLOPS | Progress: (8/20) | 4.61 s +[Task 16/25] Current/Best: 19.68/ 20.68 GFLOPS | Progress: (12/20) | 5.82 s +[Task 16/25] Current/Best: 17.95/ 20.68 GFLOPS | Progress: (16/20) | 7.18 s +[Task 16/25] Current/Best: 10.15/ 22.12 GFLOPS | Progress: (20/20) | 9.33 s Done. + +[Task 17/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 17/25] Current/Best: 13.51/ 18.93 GFLOPS | Progress: (4/20) | 4.79 s +[Task 17/25] Current/Best: 14.55/ 23.37 GFLOPS | Progress: (8/20) | 7.56 s +[Task 17/25] Current/Best: 16.89/ 23.37 GFLOPS | Progress: (12/20) | 9.61 s +[Task 17/25] Current/Best: 16.77/ 23.37 GFLOPS | Progress: (16/20) | 11.81 s +[Task 17/25] Current/Best: 10.14/ 23.37 GFLOPS | Progress: (20/20) | 13.94 s Done. + +[Task 18/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 18/25] Current/Best: 11.52/ 18.15 GFLOPS | Progress: (4/20) | 3.78 s +[Task 18/25] Current/Best: 10.71/ 19.51 GFLOPS | Progress: (8/20) | 7.45 s +[Task 18/25] Current/Best: 19.44/ 19.51 GFLOPS | Progress: (12/20) | 9.39 s +[Task 18/25] Current/Best: 10.08/ 19.51 GFLOPS | Progress: (16/20) | 13.17 s +[Task 18/25] Current/Best: 20.94/ 20.94 GFLOPS | Progress: (20/20) | 14.66 s Done. + +[Task 19/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 19/25] Current/Best: 7.21/ 20.46 GFLOPS | Progress: (4/20) | 6.09 s +[Task 19/25] Current/Best: 2.63/ 20.46 GFLOPS | Progress: (8/20) | 9.41 s +[Task 19/25] Current/Best: 19.25/ 21.00 GFLOPS | Progress: (12/20) | 12.32 s +[Task 19/25] Current/Best: 15.32/ 22.01 GFLOPS | Progress: (16/20) | 15.25 s +[Task 19/25] Current/Best: 2.72/ 23.38 GFLOPS | Progress: (20/20) | 18.04 s Done. + +[Task 20/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 20/25] Current/Best: 8.70/ 15.13 GFLOPS | Progress: (4/20) | 3.34 s Done. + Done. + +[Task 20/25] Current/Best: 10.07/ 15.13 GFLOPS | Progress: (8/20) | 6.70 s +[Task 20/25] Current/Best: 2.35/ 16.65 GFLOPS | Progress: (12/20) | 10.62 s +[Task 20/25] Current/Best: 12.51/ 16.65 GFLOPS | Progress: (16/20) | 14.49 s +[Task 20/25] Current/Best: 13.29/ 22.42 GFLOPS | Progress: (20/20) | 16.56 s +[Task 21/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 21/25] Current/Best: 6.47/ 18.00 GFLOPS | Progress: (4/20) | 3.24 s +[Task 21/25] Current/Best: 14.70/ 18.00 GFLOPS | Progress: (8/20) | 4.84 s +[Task 21/25] Current/Best: 1.63/ 18.00 GFLOPS | Progress: (12/20) | 6.96 s +[Task 21/25] Current/Best: 18.46/ 18.46 GFLOPS | Progress: (16/20) | 10.46 s +[Task 21/25] Current/Best: 4.52/ 18.46 GFLOPS | Progress: (20/20) | 17.74 s +[Task 22/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 22/25] Current/Best: 2.74/ 17.31 GFLOPS | Progress: (4/20) | 2.66 s +[Task 22/25] Current/Best: 9.18/ 22.31 GFLOPS | Progress: (8/20) | 4.69 s +[Task 22/25] Current/Best: 19.91/ 22.31 GFLOPS | Progress: (12/20) | 7.04 s +[Task 22/25] Current/Best: 15.37/ 22.31 GFLOPS | Progress: (16/20) | 9.12 s +[Task 22/25] Current/Best: 14.47/ 22.31 GFLOPS | Progress: (20/20) | 10.83 s Done. + +[Task 23/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 23/25] Current/Best: 17.75/ 20.70 GFLOPS | Progress: (4/20) | 3.23 s +[Task 23/25] Current/Best: 15.91/ 20.70 GFLOPS | Progress: (8/20) | 6.67 s +[Task 23/25] Current/Best: 21.19/ 21.55 GFLOPS | Progress: (12/20) | 8.49 s +[Task 23/25] Current/Best: 6.53/ 21.55 GFLOPS | Progress: (16/20) | 15.43 s +[Task 23/25] Current/Best: 7.96/ 21.55 GFLOPS | Progress: (20/20) | 19.61 s Done. + +[Task 24/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 24/25] Current/Best: 8.44/ 8.44 GFLOPS | Progress: (4/20) | 11.80 s +[Task 24/25] Current/Best: 2.01/ 8.44 GFLOPS | Progress: (8/20) | 22.82 s +[Task 24/25] Current/Best: 4.44/ 8.44 GFLOPS | Progress: (12/20) | 34.34 s Done. + Done. + +[Task 24/25] Current/Best: 7.18/ 8.73 GFLOPS | Progress: (16/20) | 40.14 s +[Task 24/25] Current/Best: 3.29/ 8.99 GFLOPS | Progress: (20/20) | 46.23 s Done. + +[Task 25/25] Current/Best: 0.00/ 0.00 GFLOPS | Progress: (0/20) | 0.00 s +[Task 25/25] Current/Best: 1.56/ 2.93 GFLOPS | Progress: (4/20) | 11.63 s +[Task 25/25] Current/Best: 5.65/ 7.64 GFLOPS | Progress: (8/20) | 22.93 s +[Task 25/25] Current/Best: 5.95/ 7.64 GFLOPS | Progress: (12/20) | 34.36 s +[Task 25/25] Current/Best: 5.80/ 9.36 GFLOPS | Progress: (16/20) | 36.05 s +[Task 25/25] Current/Best: 2.94/ 9.36 GFLOPS | Progress: (20/20) | 46.76 s +``` + +调优过程的输出如下所示: + +``` plain +# [Task 1/24] Current/Best: 10.71/ 21.08 GFLOPS | Progress: (60/1000) | 111.77 s Done. +# [Task 1/24] Current/Best: 9.32/ 24.18 GFLOPS | Progress: (192/1000) | 365.02 s Done. +# [Task 2/24] Current/Best: 22.39/ 177.59 GFLOPS | Progress: (960/1000) | 976.17 s Done. +# [Task 3/24] Current/Best: 32.03/ 153.34 GFLOPS | Progress: (800/1000) | 776.84 s Done. +# [Task 4/24] Current/Best: 11.96/ 156.49 GFLOPS | Progress: (960/1000) | 632.26 s Done. +# [Task 5/24] Current/Best: 23.75/ 130.78 GFLOPS | Progress: (800/1000) | 739.29 s Done. +# [Task 6/24] Current/Best: 38.29/ 198.31 GFLOPS | Progress: (1000/1000) | 624.51 s Done. +# [Task 7/24] Current/Best: 4.31/ 210.78 GFLOPS | Progress: (1000/1000) | 701.03 s Done. +# [Task 8/24] Current/Best: 50.25/ 185.35 GFLOPS | Progress: (972/1000) | 538.55 s Done. +# [Task 9/24] Current/Best: 50.19/ 194.42 GFLOPS | Progress: (1000/1000) | 487.30 s Done. +# [Task 10/24] Current/Best: 12.90/ 172.60 GFLOPS | Progress: (972/1000) | 607.32 s Done. +# [Task 11/24] Current/Best: 62.71/ 203.46 GFLOPS | Progress: (1000/1000) | 581.92 s Done. +# [Task 12/24] Current/Best: 36.79/ 224.71 GFLOPS | Progress: (1000/1000) | 675.13 s Done. +# [Task 13/24] Current/Best: 7.76/ 219.72 GFLOPS | Progress: (1000/1000) | 519.06 s Done. +# [Task 14/24] Current/Best: 12.26/ 202.42 GFLOPS | Progress: (1000/1000) | 514.30 s Done. +# [Task 15/24] Current/Best: 31.59/ 197.61 GFLOPS | Progress: (1000/1000) | 558.54 s Done. +# [Task 16/24] Current/Best: 31.63/ 206.08 GFLOPS | Progress: (1000/1000) | 708.36 s Done. +# [Task 17/24] Current/Best: 41.18/ 204.45 GFLOPS | Progress: (1000/1000) | 736.08 s Done. +# [Task 18/24] Current/Best: 15.85/ 222.38 GFLOPS | Progress: (980/1000) | 516.73 s Done. +# [Task 19/24] Current/Best: 15.78/ 203.41 GFLOPS | Progress: (1000/1000) | 587.13 s Done. +# [Task 20/24] Current/Best: 30.47/ 205.92 GFLOPS | Progress: ( +``` + +## 使用调优数据编译优化模型 + +获取存储在 `resnet-50-v2-autotuning.json`(上述调优过程的输出文件)中的调优记录。编译器会用这个结果,为指定 target 上的模型生成高性能代码。 + +收集到模型的调优数据后,可用优化的算子重新编译模型来加快计算速度。 + +``` python +with autotvm.apply_history_best(tuning_option["tuning_records"]): + with tvm.transform.PassContext(opt_level=3, config={}): + lib = relay.build(mod, target=target, params=params) + +dev = tvm.device(str(target), 0) +module = graph_executor.GraphModule(lib["default"](dev)) +``` + +输出结果: + +``` plain +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +验证优化模型是否运行并产生相同的结果: + +``` python +dtype = "float32" +module.set_input(input_name, img_data) +module.run() +output_shape = (1, 1000) +tvm_output = module.get_output(0, tvm.nd.empty(output_shape)).numpy() + +scores = softmax(tvm_output) +scores = np.squeeze(scores) +ranks = np.argsort(scores)[::-1] +for rank in ranks[0:5]: + print("class='%s' with probability=%f" % (labels[rank], scores[rank])) +``` + +输出结果: + +``` plain +class='n02123045 tabby, tabby cat' with probability=0.621104 +class='n02123159 tiger cat' with probability=0.356378 +class='n02124075 Egyptian cat' with probability=0.019712 +class='n02129604 tiger, Panthera tigris' with probability=0.001215 +class='n04040759 radiator' with probability=0.000262 +``` + +验证预测值是否相同: + +``` plain +# class='n02123045 tabby, tabby cat' with probability=0.610550 +# class='n02123159 tiger cat' with probability=0.367181 +# class='n02124075 Egyptian cat' with probability=0.019365 +# class='n02129604 tiger, Panthera tigris' with probability=0.001273 +# class='n04040759 radiator' with probability=0.000261 +``` + +## 比较调优和未调优的模型 + +收集与此优化模型相关的一些基本性能数据,并将其与未优化模型进行比较。根据底层硬件、迭代次数和其他因素,将优化模型和未优化模型比较时,可以看到性能的提升。 + +``` python +import timeit + +timing_number = 10 +timing_repeat = 10 +optimized = ( + np.array(timeit.Timer(lambda: module.run()).repeat(repeat=timing_repeat, number=timing_number)) + * 1000 + / timing_number +) +optimized = {"mean": np.mean(optimized), "median": np.median(optimized), "std": np.std(optimized)} + + + +print("optimized: %s" % (optimized)) +print("unoptimized: %s" % (unoptimized)) +``` + +输出结果: + +```plain +optimized: {'mean': 407.31687583000166, 'median': 407.3377107500164, 'std': 1.692177042688564} +unoptimized: {'mean': 495.13895513002353, 'median': 494.6680843500417, 'std': 1.3081147373726523} +``` + +## 写在最后 + +本教程通过一个简短示例,说明了如何用 TVM Python API 编译、运行和调优模型。还讨论了对输入和输出进行预处理和后处理的必要性。在调优过程之后,演示了如何比较未优化和优化模型的性能。 + +本文档展示了一个在本地使用 ResNet-50 v2 的简单示例。TVMC 还支持更多功能,包括交叉编译、远程执行和分析/基准测试等。 + +**脚本总运行时间:**(10 分 27.660 秒) + +`下载 Python 源代码:autotvm_relay_x86.py` + +`下载 Jupyter Notebook:autotvm_relay_x86.ipynb` diff --git a/versioned_docs/version-0.12.0/tutorial/06-tensor_expr.md b/versioned_docs/version-0.12.0/tutorial/06-tensor_expr.md new file mode 100644 index 00000000..04fb2d47 --- /dev/null +++ b/versioned_docs/version-0.12.0/tutorial/06-tensor_expr.md @@ -0,0 +1,1064 @@ +--- +title: 使用张量表达式处理算子 +--- + +# 使用张量表达式处理算子 + +:::note +单击 [此处](https://tvm.apache.org/docs/tutorial/tensor_expr_get_started.html#sphx-glr-download-tutorial-tensor-expr-get-started-py) 下载完整的示例代码 +::: + +**作者**:[Tianqi Chen](https://tqchen.github.io/) + +本教程重点关注 TVM 如何用张量表达式(TE)来定义张量计算并应用循环优化。 TE 用纯函数式语言描述张量计算(即每个函数表达式都不会产生副作用(side effect))。从 TVM 的整体来看,Relay 将计算描述为一组算子,每个算子都可以表示为一个 TE 表达式,其中每个 TE 表达式接收输入张量并产生一个输出张量。 + +这是 TVM 中张量表达式语言的入门教程。 TVM 使用特定领域的张量表达式来进行有效的内核构建。下面将通过两个使用张量表达式语言的示例,来演示基本工作流程。第一个例子介绍了 TE 和带有向量加法的调度。通过逐步讲解如何用 TE 优化矩阵乘法,对第二个例子的这些概念进行了扩展。后续涉及到更高阶的 TVM 功能教程,将基于此矩阵乘法示例。 + +## 示例 1:在 TE 中为 CPU 编写并调度向量加法 + +以下 Python 示例展示了如何实现用于向量加法的 TE,以及针对 CPU 的调度。首先初始化一个 TVM 环境: + +``` python +import tvm +import tvm.testing +from tvm import te +import numpy as np +``` + +为了提高性能,可以指定目标 CPU。如果使用的是 LLVM,可输入命令 `llc --version` 来获取 CPU 类型,并通过查看 `/proc/cpuinfo` 获取处理器支持的其他扩展。例如,如果 CPU 支持 AVX-512 指令,则可以使用 `llvm -mcpu=skylake-avx512`。 + +``` python +tgt = tvm.target.Target(target="llvm", host="llvm") +``` + +### 描述向量计算 + +TVM 采用张量语义,每个中间结果表示为一个多维数组。用户需要描述生成张量的计算规则。首先定义一个符号变量 `n` 来表示 shape。然后定义两个占位符张量,`A` 和 `B`,给定 shape 都为 `(n,)`。然后用 `compute` 操作描述结果张量 `C`。 + +`compute` 定义了一个计算,其输出符合指定的张量 shape,计算将在 lambda 函数定义的张量中的每个位置执行。注意,虽然 `n` 是一个变量,但它定义了 `A`、`B` 和 `C` 张量之间的一致的 shape。注意,此过程只是在声明应该如何进行计算,不会发生实际的计算。 + +``` python +n = te.var("n") +A = te.placeholder((n,), name="A") +B = te.placeholder((n,), name="B") +C = te.compute(A.shape, lambda i: A[i] + B[i], name="C") +``` + +:::note Lambda 函数 +`te.compute` 方法的第二个参数是执行计算的函数。此示例使用匿名函数(也称 `lambda` 函数)来定义计算,在本例中是对 `A` 和 `B` 的第 `i` 个元素进行加法运算。 +::: + +### 为计算创建默认 schedule + +虽然上面描述了计算规则,但为适应不同的设备,可用不同的方式来计算 `C`。对于具有多个轴(axis)的张量,可以选择首先迭代哪个轴,或将计算拆分到不同的线程。 TVM 要求用户提供 schedule,这是对如何执行计算的描述。 TE 中的调度操作可以在其他操作中更改循环顺序、跨不同线程拆分计算以及将数据块组合在一起。调度背后的一个重要概念是它们只描述如何执行计算,因此同一 TE 的不同调度将产生相同的结果。 + +TVM 允许创建一个通过按行迭代计算 `C` 的 schedule: + +``` python +for (int i = 0; i < n; ++i) { + C[i] = A[i] + B[i]; +} +s = te.create_schedule(C.op) +``` + +### 编译和评估默认 schedule + +用 TE 表达式和 schedule 可为目标语言和架构(本例为 LLVM 和 CPU)生成可运行的代码。TVM 提供 schedule、schedule 中的 TE 表达式列表、target 和 host,以及正在生成的函数名。输出结果是一个类型擦除函数(type-erased function),可直接从 Python 调用。 + +下面将使用 `tvm.build` 创建函数。 build 函数接收 schedule、所需的函数签名(包括输入和输出)以及要编译到的目标语言。 + +``` python +fadd = tvm.build(s, [A, B, C], tgt, name="myadd") +``` + +输出结果: + +``` plain +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +运行该函数,并将输出与 numpy 中的相同计算进行比较。编译后的 TVM 函数提供了一个任何语言都可调用的 C API。首先创建 TVM 编译调度的目标设备(本例为 CPU)。这个例子的目标设备为 LLVM CPU。然后初始化设备中的张量,并执行自定义的加法操作。为了验证计算是否正确,可将 C 张量的输出结果与 numpy 执行相同计算的结果进行比较。 + +``` python +dev = tvm.device(tgt.kind.name, 0) + +n = 1024 +a = tvm.nd.array(np.random.uniform(size=n).astype(A.dtype), dev) +b = tvm.nd.array(np.random.uniform(size=n).astype(B.dtype), dev) +c = tvm.nd.array(np.zeros(n, dtype=C.dtype), dev) +fadd(a, b, c) +tvm.testing.assert_allclose(c.numpy(), a.numpy() + b.numpy()) +``` + +创建一个辅助函数来运行 TVM 生成的代码的配置文件,从而比较这个版本和 numpy 的运行速度。 + +``` python +import timeit + +np_repeat = 100 +np_running_time = timeit.timeit( + setup="import numpy\n" + "n = 32768\n" + 'dtype = "float32"\n' + "a = numpy.random.rand(n, 1).astype(dtype)\n" + "b = numpy.random.rand(n, 1).astype(dtype)\n", + stmt="answer = a + b", + number=np_repeat, +) +print("Numpy running time: %f" % (np_running_time / np_repeat)) + +def evaluate_addition(func, target, optimization, log): + dev = tvm.device(target.kind.name, 0) + n = 32768 + a = tvm.nd.array(np.random.uniform(size=n).astype(A.dtype), dev) + b = tvm.nd.array(np.random.uniform(size=n).astype(B.dtype), dev) + c = tvm.nd.array(np.zeros(n, dtype=C.dtype), dev) + + evaluator = func.time_evaluator(func.entry_name, dev, number=10) + mean_time = evaluator(a, b, c).mean + print("%s: %f" % (optimization, mean_time)) + + log.append((optimization, mean_time)) + +log = [("numpy", np_running_time / np_repeat)] +evaluate_addition(fadd, tgt, "naive", log=log) +``` + +输出结果: + +``` plain +Numpy running time: 0.000008 +naive: 0.000006 +``` + +### 更新 schedule 以使用并行性 + +前面已经讲解了 TE 的基础知识,下面将更深入地了解调度的作用,以及如何使用它们来优化不同架构的张量表达式。schedule 是用多种方式来转换表达式的一系列步骤。当调度应用于 TE 中的表达式时,输入和输出保持不变,但在编译时,表达式的实现会发生变化。默认调度的这种张量加法是串行运行的,不过可以很容易实现在所有处理器线程上并行化。将并行调度操作应用于我们的计算: + +``` python +s[C].parallel(C.op.axis[0]) +``` + +`tvm.lower` 命令会生成 TE 的中间表示(IR),以及相应的 schedule。应用不同的调度操作时降低表达式,可以看到调度对计算顺序的影响。用标志 `simple_mode=True` 来返回可读的 C 风格语句: + +``` python +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [(stride: int32*n: int32)], [], type="auto"), + B: Buffer(B_2: Pointer(float32), float32, [(stride_1: int32*n)], [], type="auto"), + C: Buffer(C_2: Pointer(float32), float32, [(stride_2: int32*n)], [], type="auto")} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [n], [stride], type="auto"), B_1: B_3: Buffer(B_2, float32, [n], [stride_1], type="auto"), C_1: C_3: Buffer(C_2, float32, [n], [stride_2], type="auto")} { + for (i: int32, 0, n) "parallel" { + C[(i*stride_2)] = (A[(i*stride)] + B[(i*stride_1)]) + } +} +``` + +TVM 现在可以在独立的线程上运行这些代码块。下面将并行编译并运行这个新的 schedule: + +``` python +fadd_parallel = tvm.build(s, [A, B, C], tgt, name="myadd_parallel") +fadd_parallel(a, b, c) + +tvm.testing.assert_allclose(c.numpy(), a.numpy() + b.numpy()) + +evaluate_addition(fadd_parallel, tgt, "parallel", log=log) +``` + +输出结果: + +``` plain +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +parallel: 0.000006 +``` + +### 更新 schedule 以使用向量化 + +现代 CPU 可以对浮点值执行 SIMD 操作,利用这一优势,可将另一个调度应用于计算表达式中。实现这一点需要多个步骤:首先,用拆分调度原语将调度拆分为内部循环和外部循环。内部循环可用向量化调度原语来调用 SIMD 指令,然后可用并行调度原语对外部循环进行并行化。选择拆分因子作为 CPU 上的线程数量。 + +``` python +# 重新创建 schedule, 因为前面的例子在并行操作中修改了它 +n = te.var("n") +A = te.placeholder((n,), name="A") +B = te.placeholder((n,), name="B") +C = te.compute(A.shape, lambda i: A[i] + B[i], name="C") + +s = te.create_schedule(C.op) + +# 这个因子应该和适合 CPU 的线程数量匹配。 +# 这会因架构差异而有所不同,不过好的规则是 +# 将这个因子设置为 CPU 可用内核数量。 +factor = 4 + +outer, inner = s[C].split(C.op.axis[0], factor=factor) +s[C].parallel(outer) +s[C].vectorize(inner) + +fadd_vector = tvm.build(s, [A, B, C], tgt, name="myadd_parallel") + +evaluate_addition(fadd_vector, tgt, "vector", log=log) + +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +vector: 0.000024 +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [(stride: int32*n: int32)], [], type="auto"), + B: Buffer(B_2: Pointer(float32), float32, [(stride_1: int32*n)], [], type="auto"), + C: Buffer(C_2: Pointer(float32), float32, [(stride_2: int32*n)], [], type="auto")} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [n], [stride], type="auto"), B_1: B_3: Buffer(B_2, float32, [n], [stride_1], type="auto"), C_1: C_3: Buffer(C_2, float32, [n], [stride_2], type="auto")} { + for (i.outer: int32, 0, floordiv((n + 3), 4)) "parallel" { + for (i.inner.s: int32, 0, 4) { + if @tir.likely((((i.outer*4) + i.inner.s) < n), dtype=bool) { + let cse_var_1: int32 = ((i.outer*4) + i.inner.s) + C[(cse_var_1*stride_2)] = (A[(cse_var_1*stride)] + B[(cse_var_1*stride_1)]) + } + } + } +} +``` + +### 比较不同的 schedule + +现在可以对不同的 schedule 进行比较: + +``` python +baseline = log[0][1] +print("%s\t%s\t%s" % ("Operator".rjust(20), "Timing".rjust(20), "Performance".rjust(20))) +for result in log: + print( + "%s\t%s\t%s" + % (result[0].rjust(20), str(result[1]).rjust(20), str(result[1] / baseline).rjust(20)) + ) + +``` + +输出结果: + +``` plain +Operator Timing Performance + numpy 7.816320003257716e-06 1.0 + naive 6.4633000000000004e-06 0.8268980795702072 +parallel 6.0509e-06 0.774136677807214 + vector 2.39039e-05 3.0582038593656913 +``` + +:::note Code Specialization +由前面可知,`A`、`B` 和 `C` 的声明都采用相同的 shape 参数 `n`。基于此,TVM 只需将单个 shape 参数传递给内核(如上面打印的设备代码所示),这是 code specialization 的一种形式。 + +在宿主机上,TVM 自动生成检查代码来检查参数中的约束。因此,如果将不同 shape 的数组传递给 fadd,则会引发错误。 + +此外,代码还可以实现更多 specialization。例如,在计算声明中写成 `n = tvm.runtime.convert(1024)`,而不是 `n = te.var("n")`。生成的函数只会接收长度为 1024 的向量。 +::: + +经过上述步骤,我们已经对向量加法算子(vector addition operator)进行了定义、调度和编译,接下来在 TVM runtime 执行。将算子保存为一个库,之后可以在 TVM runtime 中加载。 + +### GPU 的目标向量加法(可选) + +TVM 适用于多种架构。下面的示例将针对 GPU 的向量加法进行编译: + +``` python +# 要运行这个代码, 更改为 `run_cuda = True` +# 注意:默认这个示例不在 CI 文档上运行 + +run_cuda = False +if run_cuda: + # 将这个 target 改为你 GPU 的正确后端。例如:NVIDIA GPUs:cuda;Radeon GPUS:rocm;opencl:OpenCL + tgt_gpu = tvm.target.Target(target="cuda", host="llvm") + + # 重新创建 schedule + n = te.var("n") + A = te.placeholder((n,), name="A") + B = te.placeholder((n,), name="B") + C = te.compute(A.shape, lambda i: A[i] + B[i], name="C") + print(type(C)) + + s = te.create_schedule(C.op) + + bx, tx = s[C].split(C.op.axis[0], factor=64) + + ################################################################################ + # 最终必须将迭代轴 bx 和 tx 和 GPU 计算网格绑定。 + # 原生 schedule 对 GPU 是无效的, 这些是允许我们生成可在 GPU 上运行的代码的特殊构造 + + s[C].bind(bx, te.thread_axis("blockIdx.x")) + s[C].bind(tx, te.thread_axis("threadIdx.x")) + + ###################################################################### + # 编译 + # ----------- + # 指定 schedule 后, 可将它编译为 TVM 函数。 + # 默认 TVM 编译为可直接从 Python 端调用的类型擦除函数。 + # + # 下面将用 tvm.build 来创建函数。 + # build 函数接收 schedule、所需的函数签名(包括输入和输出)以及要编译到的目标语言。 + # + # fadd 的编译结果是 GPU 设备函数(如果利用了 GPU)以及调用 GPU 函数的主机 wrapper。 + # fadd 是生成的主机 wrapper 函数,它包含对内部生成的设备函数的引用。 + + fadd = tvm.build(s, [A, B, C], target=tgt_gpu, name="myadd") + + ################################################################################ + # 编译后的 TVM 函数提供了一个任何语言都可调用的 C API。 + # + # 我们在 Python 中提供了最小数组 API 来进行快速测试以及制作原型。 + # 数组 API 基于 `DLPack [https://github.com/dmlc/dlpack](https://github.com/dmlc/dlpack)`_ 标准。 + # + # - 首先创建 GPU 设备。 + # - 然后 tvm.nd.array 将数据复制到 GPU 上。 + # - `fadd` 运行真实的计算。 + # - `numpy()` 将 GPU 数组复制回 CPU 上(然后验证正确性)。 + # + # 注意将数据复制进出内存是必要步骤。 + + dev = tvm.device(tgt_gpu.kind.name, 0) + + n = 1024 + a = tvm.nd.array(np.random.uniform(size=n).astype(A.dtype), dev) + b = tvm.nd.array(np.random.uniform(size=n).astype(B.dtype), dev) + c = tvm.nd.array(np.zeros(n, dtype=C.dtype), dev) + fadd(a, b, c) + tvm.testing.assert_allclose(c.numpy(), a.numpy() + b.numpy()) + + ################################################################################ + # 检查生成的 GPU 代码 + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # 可以在 TVM 中检查生成的代码。tvm.build 的结果是一个 TVM 模块。fadd 是包含主机模块的主机 wrapper,对 CUDA(GPU)函数来说它还包含设备模块。 + # + # 下面的代码从设备模块中取出并打印内容代码。 + + if ( + tgt_gpu.kind.name == "cuda" + or tgt_gpu.kind.name == "rocm" + or tgt_gpu.kind.name.startswith("opencl") + ): + dev_module = fadd.imported_modules[0] + print("-----GPU code-----") + print(dev_module.get_source()) + else: + print(fadd.get_source()) +``` + +## 保存和加载已编译的模块 + +除了 runtime 编译外,还可将编译后的模块保存到文件中,之后再进行加载。 + +以下代码首先执行: + +* 将编译的主机模块保存到目标文件中。 +* 然后将设备模块保存到 ptx 文件中。 +* cc.create_shared 调用编译器(gcc)来创建共享库。 + +``` python +from tvm.contrib import cc +from tvm.contrib import utils + +temp = utils.tempdir() +fadd.save(temp.relpath("myadd.o")) +if tgt.kind.name == "cuda": + fadd.imported_modules[0].save(temp.relpath("myadd.ptx")) +if tgt.kind.name == "rocm": + fadd.imported_modules[0].save(temp.relpath("myadd.hsaco")) +if tgt.kind.name.startswith("opencl"): + fadd.imported_modules[0].save(temp.relpath("myadd.cl")) +cc.create_shared(temp.relpath("myadd.so"), [temp.relpath("myadd.o")]) +print(temp.listdir()) +``` + +输出结果: + +``` plain +['myadd.o', 'myadd.so'] +``` + +模块存储格式: + +CPU(主机)模块直接保存为共享库(.so)。设备代码有多种自定义格式。在我们的示例中,设备代码存储在 ptx 以及元数据 json 文件中。它们可以分别导入,从而实现单独加载和链接。 + +### 加载编译模块 + +可从文件系统中加载编译好的模块并运行代码。下面的代码分别加载主机和设备模块,并将它们链接在一起。可以验证新加载的函数是否有效。 + +``` python +fadd1 = tvm.runtime.load_module(temp.relpath("myadd.so")) +if tgt.kind.name == "cuda": + fadd1_dev = tvm.runtime.load_module(temp.relpath("myadd.ptx")) + fadd1.import_module(fadd1_dev) + +if tgt.kind.name == "rocm": + fadd1_dev = tvm.runtime.load_module(temp.relpath("myadd.hsaco")) + fadd1.import_module(fadd1_dev) + +if tgt.kind.name.startswith("opencl"): + fadd1_dev = tvm.runtime.load_module(temp.relpath("myadd.cl")) + fadd1.import_module(fadd1_dev) + +fadd1(a, b, c) +tvm.testing.assert_allclose(c.numpy(), a.numpy() + b.numpy()) +``` + +### 将所有内容打包到一个库中 + +上面的示例分别存储了设备和主机代码。 TVM 还支持将所有内容导出为一个共享库。在后台,将设备模块打包成二进制 blob,并将它们与主机代码链接在一起。目前支持 Metal、OpenCL 和 CUDA 模块的打包。 + +``` python +fadd.export_library(temp.relpath("myadd_pack.so")) +fadd2 = tvm.runtime.load_module(temp.relpath("myadd_pack.so")) +fadd2(a, b, c) +tvm.testing.assert_allclose(c.numpy(), a.numpy() + b.numpy()) +``` + +:::note runtime API 和线程安全 +TVM 的编译模块不依赖于 TVM 编译器,它们只依赖于最小的 runtime 库。 TVM runtime 库封装了设备驱动程序,并在编译后的函数中提供线程安全和 device-agnostic 的调用。 + +这意味着,可以从任何线程,在任何已经编译了代码的 GPU 上调用已编译的 TVM 函数。 +::: + +## 生成 OpenCL 代码 + +TVM 为多个后端提供代码生成功能。可以生成在 CPU 后端运行的 OpenCL 代码或 LLVM 代码。 + +下面的代码块首先生成 OpenCL 代码,然后在 OpenCL 设备上创建数组,最后验证代码的正确性。 + +``` python +if tgt.kind.name.startswith("opencl"): + fadd_cl = tvm.build(s, [A, B, C], tgt, name="myadd") + print("------opencl code------") + print(fadd_cl.imported_modules[0].get_source()) + dev = tvm.cl(0) + n = 1024 + a = tvm.nd.array(np.random.uniform(size=n).astype(A.dtype), dev) + b = tvm.nd.array(np.random.uniform(size=n).astype(B.dtype), dev) + c = tvm.nd.array(np.zeros(n, dtype=C.dtype), dev) + fadd_cl(a, b, c) + tvm.testing.assert_allclose(c.numpy(), a.numpy() + b.numpy()) +``` + +:::note TE 调度原语 +TVM 的调度原语包括: +* split:将指定的轴按定义的因子拆分为两个轴。 +* tile:通过定义的因子将计算拆分到两个轴上。 +* fuse:将一个计算的两个连续轴融合。 +* reorder:可以将计算的轴重新排序为定义的顺序。 +* bind:可以将计算绑定到特定线程,在 GPU 编程中很有用。 +* compute_at:TVM 默认将在函数的最外层或根部计算张量。 compute_at 指定应该在另一个算子的第一个计算轴上计算一个张量。 +* compute_inline:当标记为 inline 时,计算将被扩展,然后插入到需要张量的地址中。 +* compute_root:将计算移动到函数的最外层或根部。这意味着当前阶段的计算完全完成后才可进入下一阶段。 + +原语的完整描述参考 [Schedule Primitives](https://tvm.apache.org/docs/how_to/work_with_schedules/schedule_primitives.html#schedule-primitives) 文档。 +::: + +## 示例 2:使用 TE 手动优化矩阵乘法 + +现在来看第二个更高级的示例,演示了 TVM 如何仅使用 18 行 Python 代码,将常见的矩阵乘法运算速度提高 18 倍。 + +**矩阵乘法是计算密集型运算。为取得良好的 CPU 性能,有两个重要的优化:** + +1. 提高内存访问的缓存命中率。高缓存命中率可以加速复杂的数值计算和热点内存访问。这需要将原始内存访问模式转换为适合缓存策略的模式。 +2. SIMD(单指令多数据),又称向量处理单元。在每个循环中,SIMD 可以处理一小批数据,而不是处理单个值。这需要将循环体中的数据访问模式转换为统一模式,以便 LLVM 后端可将其降低到 SIMD。 + +本教程使用的技术出自此 [仓库](https://github.com/flame/how-to-optimize-gemm)。其中一些已经自动被 TVM 抽象地应用,但另一些由于 TVM 的限制而无法自动应用。 + +### 准备和性能基线 + +首先收集有关矩阵乘法的 numpy 实现的性能数据: + +``` python +import tvm +import tvm.testing +from tvm import te +import numpy + +# 矩阵的大小 +# (M, K) x (K, N) +# 可尝试不同的 shape,TVM 优化的性能有时比 numpy + MKL 更好 +M = 1024 +K = 1024 +N = 1024 + +# TVM 默认张量数据类型 +dtype = "float32" + +# 你可能想调整 target 使其和你的任何 CPU 向量扩展匹配 +# 例如,如果你为 SIMD 用的是 Intel AVX2(高级向量扩展)ISA,把下面这行换成 `llvm -mcpu=core-avx2` 可以取得最佳性能(或者你所用 CPU 的具体类型) +# 记住你用的是 llvm, 可以用 `llc --version` 命令来获取 CPU 类型,也可以查看 `/proc/cpuinfo` 来获取你处理器支持的更多扩展 + +target = tvm.target.Target(target="llvm", host="llvm") +dev = tvm.device(target.kind.name, 0) + +# 为测试随机生成的张量 +a = tvm.nd.array(numpy.random.rand(M, K).astype(dtype), dev) +b = tvm.nd.array(numpy.random.rand(K, N).astype(dtype), dev) + +# 重复执行矩阵乘法以获得默认 numpy 实现的性能基线 +np_repeat = 100 +np_running_time = timeit.timeit( + setup="import numpy\n" + "M = " + str(M) + "\n" + "K = " + str(K) + "\n" + "N = " + str(N) + "\n" + 'dtype = "float32"\n' + "a = numpy.random.rand(M, K).astype(dtype)\n" + "b = numpy.random.rand(K, N).astype(dtype)\n", + stmt="answer = numpy.dot(a, b)", + number=np_repeat, +) +print("Numpy running time: %f" % (np_running_time / np_repeat)) + +answer = numpy.dot(a.numpy(), b.numpy()) +``` + +输出结果: + +``` plain +Numpy running time: 0.019016 +``` + +用 TVM TE 编写一个基本的矩阵乘法,并验证它是否产生与 numpy 实现相同的结果。再编写一个函数来辅助评估调度优化的性能: + +``` python +# 用 TE 的 TVM 矩阵乘法 +k = te.reduce_axis((0, K), "k") +A = te.placeholder((M, K), name="A") +B = te.placeholder((K, N), name="B") +C = te.compute((M, N), lambda x, y: te.sum(A[x, k] * B[k, y], axis=k), name="C") + +# 默认 schedule +s = te.create_schedule(C.op) +func = tvm.build(s, [A, B, C], target=target, name="mmult") + +c = tvm.nd.array(numpy.zeros((M, N), dtype=dtype), dev) +func(a, b, c) +tvm.testing.assert_allclose(c.numpy(), answer, rtol=1e-5) + +def evaluate_operation(s, vars, target, name, optimization, log): + func = tvm.build(s, [A, B, C], target=target, name="mmult") + assert func + + c = tvm.nd.array(numpy.zeros((M, N), dtype=dtype), dev) + func(a, b, c) + tvm.testing.assert_allclose(c.numpy(), answer, rtol=1e-5) + + evaluator = func.time_evaluator(func.entry_name, dev, number=10) + mean_time = evaluator(a, b, c).mean + print("%s: %f" % (optimization, mean_time)) + log.append((optimization, mean_time)) + +log = [] + +evaluate_operation(s, [A, B, C], target=target, name="mmult", optimization="none", log=log) +``` + +输出结果: + +``` plain +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +none: 3.256676 +``` + +查看用 TVM 底层函数的算子和默认调度的中间表示。注意,该实现本质上是矩阵乘法的简单实现,在 A 和 B 矩阵的索引上使用三个嵌套循环。 + +``` python +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []), + B: Buffer(B_2: Pointer(float32), float32, [1048576], []), + C: Buffer(C_2: Pointer(float32), float32, [1048576], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [1024, 1024], []), B_1: B_3: Buffer(B_2, float32, [1024, 1024], []), C_1: C_3: Buffer(C_2, float32, [1024, 1024], [])} { + for (x: int32, 0, 1024) { + for (y: int32, 0, 1024) { + C[((x*1024) + y)] = 0f32 + for (k: int32, 0, 1024) { + let cse_var_2: int32 = (x*1024) + let cse_var_1: int32 = (cse_var_2 + y) + C[cse_var_1] = (C[cse_var_1] + (A[(cse_var_2 + k)]*B[((k*1024) + y)])) + } + } + } +} +``` + +### 优化一:块操作 + +提高缓存命中率的一个重要技巧是块操作,可以在其中构造内存访问,使块内部是一个具有高内存局部性的小邻域。本教程选择一个 32 的块因子,使得一个块将填充 32*32*sizeof(float) 的内存区域。这对应于 4KB 的缓存大小,而 L1 缓存的参考缓存大小为 32 KB。 + +首先为 `C` 操作创建一个默认 schedule,然后用指定的块因子对其应用 `tile` 调度原语,调度原语返回向量 `[x_outer, y_outer, x_inner, y_inner]`(从最外层到最内层的结果循环的顺序)。然后得到操作输出的归约轴,并用因子 4 对其执行拆分操作。这个因子并不直接影响现在正在处理的块优化,但在应用向量化时很有用。 + +既然操作已经块级化了,可对计算进行重新排序,将归约操作放到计算的最外层循环中,保证块数据保留在缓存中。完成 schedule 后,就可以构建和测试与原始 schedule 相比的性能。 + +``` bash +bn = 32 + +# 通过循环切分实现块级化 +xo, yo, xi, yi = s[C].tile(C.op.axis[0], C.op.axis[1], bn, bn) +(k,) = s[C].op.reduce_axis +ko, ki = s[C].split(k, factor=4) + +# 将归约域提升到块循环外 +s[C].reorder(xo, yo, ko, ki, xi, yi) + +evaluate_operation(s, [A, B, C], target=target, name="mmult", optimization="blocking", log=log) +``` + +输出结果: + +``` plain +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +blocking: 0.297447 +``` + +利用缓存对计算重新排序,可发现计算性能的显着提高。现在,打印内部表示,并将其与原始表示进行比较: + +``` python +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []), + B: Buffer(B_2: Pointer(float32), float32, [1048576], []), + C: Buffer(C_2: Pointer(float32), float32, [1048576], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [1024, 1024], []), B_1: B_3: Buffer(B_2, float32, [1024, 1024], []), C_1: C_3: Buffer(C_2, float32, [1024, 1024], [])} { + for (x.outer: int32, 0, 32) { + for (y.outer: int32, 0, 32) { + for (x.inner.init: int32, 0, 32) { + for (y.inner.init: int32, 0, 32) { + C[((((x.outer*32768) + (x.inner.init*1024)) + (y.outer*32)) + y.inner.init)] = 0f32 + } + } + for (k.outer: int32, 0, 256) { + for (k.inner: int32, 0, 4) { + for (x.inner: int32, 0, 32) { + for (y.inner: int32, 0, 32) { + let cse_var_3: int32 = (y.outer*32) + let cse_var_2: int32 = ((x.outer*32768) + (x.inner*1024)) + let cse_var_1: int32 = ((cse_var_2 + cse_var_3) + y.inner) + C[cse_var_1] = (C[cse_var_1] + (A[((cse_var_2 + (k.outer*4)) + k.inner)]*B[((((k.outer*4096) + (k.inner*1024)) + cse_var_3) + y.inner)])) + } + } + } + } + } + } +} +``` + +### 优化二:向量化 + +另一个重要的优化技巧是向量化。当内存访问模式一致时,编译器可以检测到这种模式,并将连续内存传递给 SIMD 向量处理器。利用 TVM 中这个硬件特性,可用 `vectorize` 接口来提示编译器这个模式。 + +本教程选择对内部循环行数据进行向量化,因为它在之前的优化中已经可以很好地缓存了。 + +``` python +# Apply the vectorization optimization +s[C].vectorize(yi) + +evaluate_operation(s, [A, B, C], target=target, name="mmult", optimization="vectorization", log=log) + +# The generalized IR after vectorization +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +vectorization: 0.332722 +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []), + B: Buffer(B_2: Pointer(float32), float32, [1048576], []), + C: Buffer(C_2: Pointer(float32), float32, [1048576], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [1024, 1024], []), B_1: B_3: Buffer(B_2, float32, [1024, 1024], []), C_1: C_3: Buffer(C_2, float32, [1024, 1024], [])} { + for (x.outer: int32, 0, 32) { + for (y.outer: int32, 0, 32) { + for (x.inner.init: int32, 0, 32) { + C[ramp((((x.outer*32768) + (x.inner.init*1024)) + (y.outer*32)), 1, 32)] = broadcast(0f32, 32) + } + for (k.outer: int32, 0, 256) { + for (k.inner: int32, 0, 4) { + for (x.inner: int32, 0, 32) { + let cse_var_3: int32 = (y.outer*32) + let cse_var_2: int32 = ((x.outer*32768) + (x.inner*1024)) + let cse_var_1: int32 = (cse_var_2 + cse_var_3) + C[ramp(cse_var_1, 1, 32)] = (C[ramp(cse_var_1, 1, 32)] + (broadcast(A[((cse_var_2 + (k.outer*4)) + k.inner)], 32)*B[ramp((((k.outer*4096) + (k.inner*1024)) + cse_var_3), 1, 32)])) + } + } + } + } + } +} +``` + +### 优化三:循环置换 + +查看上面的 IR,可看到内部循环行数据被向量化,并且 B 被转换为 PackedB(通过内部循环的 *(float32x32*)B2* 部分可明显看出)。 PackedB 的遍历现在是顺序的。在当前 schedule 中,A 是逐列访问的,这对缓存不利。如果我们改变 *ki* 和内轴 *xi* 的嵌套循环顺序,A 矩阵的访问模式将更利于缓存。 + +``` python +s = te.create_schedule(C.op) +xo, yo, xi, yi = s[C].tile(C.op.axis[0], C.op.axis[1], bn, bn) +(k,) = s[C].op.reduce_axis +ko, ki = s[C].split(k, factor=4) + +# re-ordering +# 重新排序 +s[C].reorder(xo, yo, ko, xi, ki, yi) +s[C].vectorize(yi) + +evaluate_operation( + s, [A, B, C], target=target, name="mmult", optimization="loop permutation", log=log +) + +# Again, print the new generalized IR +# 再次打印新生成的 IR +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +loop permutation: 0.114844 +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []), + B: Buffer(B_2: Pointer(float32), float32, [1048576], []), + C: Buffer(C_2: Pointer(float32), float32, [1048576], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [1024, 1024], []), B_1: B_3: Buffer(B_2, float32, [1024, 1024], []), C_1: C_3: Buffer(C_2, float32, [1024, 1024], [])} { + for (x.outer: int32, 0, 32) { + for (y.outer: int32, 0, 32) { + for (x.inner.init: int32, 0, 32) { + C[ramp((((x.outer*32768) + (x.inner.init*1024)) + (y.outer*32)), 1, 32)] = broadcast(0f32, 32) + } + for (k.outer: int32, 0, 256) { + for (x.inner: int32, 0, 32) { + for (k.inner: int32, 0, 4) { + let cse_var_3: int32 = (y.outer*32) + let cse_var_2: int32 = ((x.outer*32768) + (x.inner*1024)) + let cse_var_1: int32 = (cse_var_2 + cse_var_3) + C[ramp(cse_var_1, 1, 32)] = (C[ramp(cse_var_1, 1, 32)] + (broadcast(A[((cse_var_2 + (k.outer*4)) + k.inner)], 32)*B[ramp((((k.outer*4096) + (k.inner*1024)) + cse_var_3), 1, 32)])) + } + } + } + } + } +} +``` + +### 优化四:数组打包 + +另一个重要的技巧是数组打包。它对数组的存储维度进行重新排序,将某个维度上的连续访问模式转换为展开后的顺序模式。 + + ![https://github.com/dmlc/web-data/raw/main/tvm/tutorial/array-packing.png](https://github.com/dmlc/web-data/raw/main/tvm/tutorial/array-packing.png) + +如上图所示,将计算块级化(block)后,可以看到 B 的数组访问模式(展开后)是有规律但不连续的。我们期望经过一些转换后,可得到一个持续访问模式。通过将 `[16] [16]` 数组重新排序为 `[16/4] [16][4]` 数组,当从打包数组中获取相应值时,B 的访问模式是顺序的。 + +考虑到 B 的新包装,必须从一个新的默认 schedule 开始来实现这一点。值得讨论的是:TE 是一种用于编写优化算子的强大且富有表现力的语言,但它通常需要一些关于你正在编写的底层算法、数据结构和硬件目标的知识。本教程后面将讨论,如何借助 TVM 完成部分任务。下面继续新的优化 schedule: + +``` bash +# We have to re-write the algorithm slightly. +# 我们必须稍作改动以重写算法。 +packedB = te.compute((N / bn, K, bn), lambda x, y, z: B[y, x * bn + z], name="packedB") +C = te.compute( + (M, N), + lambda x, y: te.sum(A[x, k] * packedB[y // bn, k, tvm.tir.indexmod(y, bn)], axis=k), + name="C", +) + +s = te.create_schedule(C.op) + +xo, yo, xi, yi = s[C].tile(C.op.axis[0], C.op.axis[1], bn, bn) +(k,) = s[C].op.reduce_axis +ko, ki = s[C].split(k, factor=4) + +s[C].reorder(xo, yo, ko, xi, ki, yi) +s[C].vectorize(yi) + +x, y, z = s[packedB].op.axis +s[packedB].vectorize(z) +s[packedB].parallel(x) + +evaluate_operation(s, [A, B, C], target=target, name="mmult", optimization="array packing", log=log) + +# Here is the generated IR after array packing. +# 数组打包后生成的 IR。 +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +array packing: 0.106744 +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []), + B: Buffer(B_2: Pointer(float32), float32, [1048576], []), + C: Buffer(C_2: Pointer(float32), float32, [1048576], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [1024, 1024], []), B_1: B_3: Buffer(B_2, float32, [1024, 1024], []), C_1: C_3: Buffer(C_2, float32, [1024, 1024], [])} { + allocate(packedB: Pointer(global float32x32), float32x32, [32768]), storage_scope = global { + for (x: int32, 0, 32) "parallel" { + for (y: int32, 0, 1024) { + packedB_1: Buffer(packedB, float32x32, [32768], [])[((x*1024) + y)] = B[ramp(((y*1024) + (x*32)), 1, 32)] + } + } + for (x.outer: int32, 0, 32) { + for (y.outer: int32, 0, 32) { + for (x.inner.init: int32, 0, 32) { + C[ramp((((x.outer*32768) + (x.inner.init*1024)) + (y.outer*32)), 1, 32)] = broadcast(0f32, 32) + } + for (k.outer: int32, 0, 256) { + for (x.inner: int32, 0, 32) { + for (k.inner: int32, 0, 4) { + let cse_var_3: int32 = ((x.outer*32768) + (x.inner*1024)) + let cse_var_2: int32 = (k.outer*4) + let cse_var_1: int32 = (cse_var_3 + (y.outer*32)) + C[ramp(cse_var_1, 1, 32)] = (C[ramp(cse_var_1, 1, 32)] + (broadcast(A[((cse_var_3 + cse_var_2) + k.inner)], 32)*packedB_1[(((y.outer*1024) + cse_var_2) + k.inner)])) + } + } + } + } + } + } +} +``` + +### 优化五:通过缓存优化块写入 + +到目前为止,所有的优化都集中在有效地访问和计算来自 A 和 B 矩阵的数据,从而计算 C 矩阵。分块优化后,算子会逐块将结果写入C,访问模式不是顺序的。可用顺序缓存数组来解决这个问题,用 cache_write、compute_at 和 unroll 的组合来保存块结果,并在所有块结果准备好时写入 C。 + +``` python +s = te.create_schedule(C.op) + +# Allocate write cache +# 分配写缓存 +CC = s.cache_write(C, "global") + +xo, yo, xi, yi = s[C].tile(C.op.axis[0], C.op.axis[1], bn, bn) + +# Write cache is computed at yo +# 写缓存在 yo 处被计算 +s[CC].compute_at(s[C], yo) + +# New inner axes +# 新的内部轴 +xc, yc = s[CC].op.axis + +(k,) = s[CC].op.reduce_axis +ko, ki = s[CC].split(k, factor=4) +s[CC].reorder(ko, xc, ki, yc) +s[CC].unroll(ki) +s[CC].vectorize(yc) + +x, y, z = s[packedB].op.axis +s[packedB].vectorize(z) +s[packedB].parallel(x) + +evaluate_operation(s, [A, B, C], target=target, name="mmult", optimization="block caching", log=log) + +# Here is the generated IR after write cache blocking. +# 写缓存块级化后生成的 IR。 +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +block caching: 0.108552 +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []), + B: Buffer(B_2: Pointer(float32), float32, [1048576], []), + C: Buffer(C_2: Pointer(float32), float32, [1048576], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [1024, 1024], []), B_1: B_3: Buffer(B_2, float32, [1024, 1024], []), C_1: C_3: Buffer(C_2, float32, [1024, 1024], [])} { + allocate(packedB: Pointer(global float32x32), float32x32, [32768]), storage_scope = global; + allocate(C.global: Pointer(global float32), float32, [1024]), storage_scope = global { + for (x: int32, 0, 32) "parallel" { + for (y: int32, 0, 1024) { + packedB_1: Buffer(packedB, float32x32, [32768], [])[((x*1024) + y)] = B[ramp(((y*1024) + (x*32)), 1, 32)] + } + } + for (x.outer: int32, 0, 32) { + for (y.outer: int32, 0, 32) { + for (x.c.init: int32, 0, 32) { + C.global_1: Buffer(C.global, float32, [1024], [])[ramp((x.c.init*32), 1, 32)] = broadcast(0f32, 32) + } + for (k.outer: int32, 0, 256) { + for (x.c: int32, 0, 32) { + let cse_var_4: int32 = (k.outer*4) + let cse_var_3: int32 = (x.c*32) + let cse_var_2: int32 = ((y.outer*1024) + cse_var_4) + let cse_var_1: int32 = (((x.outer*32768) + (x.c*1024)) + cse_var_4) + { + C.global_1[ramp(cse_var_3, 1, 32)] = (C.global_1[ramp(cse_var_3, 1, 32)] + (broadcast(A[cse_var_1], 32)*packedB_1[cse_var_2])) + C.global_1[ramp(cse_var_3, 1, 32)] = (C.global_1[ramp(cse_var_3, 1, 32)] + (broadcast(A[(cse_var_1 + 1)], 32)*packedB_1[(cse_var_2 + 1)])) + C.global_1[ramp(cse_var_3, 1, 32)] = (C.global_1[ramp(cse_var_3, 1, 32)] + (broadcast(A[(cse_var_1 + 2)], 32)*packedB_1[(cse_var_2 + 2)])) + C.global_1[ramp(cse_var_3, 1, 32)] = (C.global_1[ramp(cse_var_3, 1, 32)] + (broadcast(A[(cse_var_1 + 3)], 32)*packedB_1[(cse_var_2 + 3)])) + } + } + } + for (x.inner: int32, 0, 32) { + for (y.inner: int32, 0, 32) { + C[((((x.outer*32768) + (x.inner*1024)) + (y.outer*32)) + y.inner)] = C.global_1[((x.inner*32) + y.inner)] + } + } + } + } + } +} +``` + +### 优化六:并行化 + +到目前为止,仅设计了用单核来计算。几乎所有现代处理器都有多个内核,计算可以从并行计算中受益。最后的优化将利用线程级并行(thread-level parallelization)。 + +``` python +# parallel +# 并行化 +s[C].parallel(xo) + +x, y, z = s[packedB].op.axis +s[packedB].vectorize(z) +s[packedB].parallel(x) + +evaluate_operation( + s, [A, B, C], target=target, name="mmult", optimization="parallelization", log=log +) + +# Here is the generated IR after parallelization. +# 并行化后生成的 IR。 +print(tvm.lower(s, [A, B, C], simple_mode=True)) +``` + +输出结果: + +``` bash +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +parallelization: 0.141811 +@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []), + B: Buffer(B_2: Pointer(float32), float32, [1048576], []), + C: Buffer(C_2: Pointer(float32), float32, [1048576], [])} + buffer_map = {A_1: A, B_1: B, C_1: C} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [1024, 1024], []), B_1: B_3: Buffer(B_2, float32, [1024, 1024], []), C_1: C_3: Buffer(C_2, float32, [1024, 1024], [])} { + allocate(packedB: Pointer(global float32x32), float32x32, [32768]), storage_scope = global { + for (x: int32, 0, 32) "parallel" { + for (y: int32, 0, 1024) { + packedB_1: Buffer(packedB, float32x32, [32768], [])[((x*1024) + y)] = B[ramp(((y*1024) + (x*32)), 1, 32)] + } + } + for (x.outer: int32, 0, 32) "parallel" { + allocate(C.global: Pointer(global float32), float32, [1024]), storage_scope = global; + for (y.outer: int32, 0, 32) { + for (x.c.init: int32, 0, 32) { + C.global_1: Buffer(C.global, float32, [1024], [])[ramp((x.c.init*32), 1, 32)] = broadcast(0f32, 32) + } + for (k.outer: int32, 0, 256) { + for (x.c: int32, 0, 32) { + let cse_var_4: int32 = (k.outer*4) + let cse_var_3: int32 = (x.c*32) + let cse_var_2: int32 = ((y.outer*1024) + cse_var_4) + let cse_var_1: int32 = (((x.outer*32768) + (x.c*1024)) + cse_var_4) + { + C.global_1[ramp(cse_var_3, 1, 32)] = (C.global_1[ramp(cse_var_3, 1, 32)] + (broadcast(A[cse_var_1], 32)*packedB_1[cse_var_2])) + C.global_1[ramp(cse_var_3, 1, 32)] = (C.global_1[ramp(cse_var_3, 1, 32)] + (broadcast(A[(cse_var_1 + 1)], 32)*packedB_1[(cse_var_2 + 1)])) + C.global_1[ramp(cse_var_3, 1, 32)] = (C.global_1[ramp(cse_var_3, 1, 32)] + (broadcast(A[(cse_var_1 + 2)], 32)*packedB_1[(cse_var_2 + 2)])) + C.global_1[ramp(cse_var_3, 1, 32)] = (C.global_1[ramp(cse_var_3, 1, 32)] + (broadcast(A[(cse_var_1 + 3)], 32)*packedB_1[(cse_var_2 + 3)])) + } + } + } + for (x.inner: int32, 0, 32) { + for (y.inner: int32, 0, 32) { + C[((((x.outer*32768) + (x.inner*1024)) + (y.outer*32)) + y.inner)] = C.global_1[((x.inner*32) + y.inner)] + } + } + } + } + } +} +``` + +### 矩阵乘法示例总结 + +仅用 18 行代码应用上述简单优化后,生成的代码开始接近带有数学内核库(MKL)的 numpy 的性能。由于一直在记录性能,因此可比较结果: + +``` python +baseline = log[0][1] +print("%s\t%s\t%s" % ("Operator".rjust(20), "Timing".rjust(20), "Performance".rjust(20))) +for result in log: + print( + "%s\t%s\t%s" + % (result[0].rjust(20), str(result[1]).rjust(20), str(result[1] / baseline).rjust(20)) + ) +``` + +输出结果: + +``` plain + Operator Timing Performance + none 3.2566761794000003 1.0 + blocking 0.29744742350000003 0.09133466366152523 + vectorization 0.33272212060000006 0.10216616644437143 +loop permutation 0.11484386070000001 0.03526413262283832 + array packing 0.10674374140000001 0.032776897523678926 + block caching 0.1085523429 0.03333224948388923 + parallelization 0.1418105982 0.04354458054411991 +``` + +注意,网页上的输出反映的是非专用 Docker 容器上的运行时间,这是不可靠的。强烈推荐你运行本教程,观察 TVM 获得的性能提升,并仔细研究每个示例,来了解对矩阵乘法运算所做的迭代改进。 + +## 总结 + +如前所述,如何使用 TE 和调度原语来应用优化,需要一些底层架构和算法的知识。但是,TE 是能搜索潜在优化的、更复杂算法的基础。学完本章节对 TE 的介绍,现在可以开始探索 TVM 如何自动化调度优化过程。 + +本教程用向量加法和矩阵乘法这两个示例讲解了 TVM 张量表达式(TE)的工作流程。一般工作流程如下: + +* 通过一系列操作描述计算。 +* 描述如何用调度原语进行计算。 +* 编译成想要的目标函数。 +* 保存之后要加载的函数(可选)。 + 接下来的教程将详细介绍矩阵乘法示例,并展示如何用可调参数构建矩阵乘法和其他操作的通用模板,从而自动优化特定平台的计算。 + +[下载 Python 源代码:tensor_expr_get_started.py](https://tvm.apache.org/docs/_downloads/40a01cffb015a67aaec0fad7e27cf80d/tensor_expr_get_started.py) + +[下载 Jupyter Notebook:tensor_expr_get_started.ipynb](https://tvm.apache.org/docs/_downloads/4459ebf5b03d332f7f380abdaef81c05/tensor_expr_get_started.ipynb) diff --git a/versioned_docs/version-0.12.0/tutorial/07-ops_AutoTVM.md b/versioned_docs/version-0.12.0/tutorial/07-ops_AutoTVM.md new file mode 100644 index 00000000..2aa51a8d --- /dev/null +++ b/versioned_docs/version-0.12.0/tutorial/07-ops_AutoTVM.md @@ -0,0 +1,304 @@ +--- +title: 用 Schedule 模板和 AutoTVM 优化算子 +--- + +# 用 Schedule 模板和 AutoTVM 优化算子 + +:::note +注意:单击 [此处](https://tvm.apache.org/docs/tutorial/autotvm_matmul_x86.html#sphx-glr-download-tutorial-autotvm-matmul-x86-py) 下载完整的示例代码 +::: + +**作者**:[Lianmin Zheng](https://github.com/merrymercy),[Chris Hoge](https://github.com/hogepodge) + +本教程将展示如何用 TVM 张量表达式(TE)语言编写 schedule 模板,并通过 AutoTVM 对模板进行搜索,从而找到最佳 schedule。这个自动优化张量计算的过程被称为 Auto-Tuning。 + +本教程基于前面的 [TE 编写矩阵乘法教程](https://tvm.apache.org/docs/tutorial/tensor_expr_get_started.html) 设立。 + +auto-tuning 包括两个步骤: + +* 第一步:定义搜索空间。 +* 第二步:运行搜索算法来探索这个空间。 + +通过本教程可以了解如何在 TVM 中执行这两个步骤。整个工作流程由一个矩阵乘法示例来说明。 + +:::note +注意,本教程不会在 Windows 或最新版本的 macOS 上运行。如需运行,请将本教程的主体放在 `if __name__ == "__main__":` 代码块中。 +::: + +## 安装依赖 + +要在 TVM 中使用 autotvm 包,需安装一些额外的依赖。 + +``` bash +pip3 install --user psutil xgboost cloudpickle +``` + +为了让 TVM 在调优过程中运行更快,建议使用 Cython 作为 TVM 的 FFI。在 TVM 的根目录下,执行: + +``` bash +pip3 install --user cython +sudo make cython3 +``` + +现在我们一起来看如何用 Python 代码实现。首先导入所需的包: + +``` python +import logging +import sys + +import numpy as np +import tvm +from tvm import te +import tvm.testing + +# 模块名叫 `autotvm` +from tvm import autotvm +``` + +## TE 的基本矩阵乘法 + +回想一下用 TE 进行矩阵乘法的基本实现,下面做一些改变。将矩阵乘法放在 Python 函数定义中。简单起见,重点关注拆分的优化,将重新排序的块大小设为固定值。 + +``` python +def matmul_basic(N, L, M, dtype): + + A = te.placeholder((N, L), name="A", dtype=dtype) + B = te.placeholder((L, M), name="B", dtype=dtype) + + k = te.reduce_axis((0, L), name="k") + C = te.compute((N, M), lambda i, j: te.sum(A[i, k] * B[k, j], axis=k), name="C") + s = te.create_schedule(C.op) + + # 调度 + y, x = s[C].op.axis + k = s[C].op.reduce_axis[0] + + yo, yi = s[C].split(y, 8) + xo, xi = s[C].split(x, 8) + + s[C].reorder(yo, xo, k, yi, xi) + + return s, [A, B, C] +``` + +## 用 AutoTVM 进行矩阵乘法 + +前面的调度代码用常量“8”作为循环切分因子,但是它可能不是最佳的。因为最佳的循环切分因子取决于真实的硬件环境和输入 shape。 + +如果希望调度代码能够在更广泛的输入 shape 和目标硬件上可移植,最好定义一组候选值,并根据目标硬件上的评估结果选择最佳值。 + +autotvm 中可以为这种值定义一个可调参数,或者一个 "knob"。 + +## 基本矩阵乘法模板 + +以下示例将演示,如何为 *split* 调度操作的 block 大小创建一个可调的参数集。 + +``` python +# Matmul V1: 列出候选值 +@autotvm.template("tutorial/matmul_v1") # 1. 使用装饰器 +def matmul_v1(N, L, M, dtype): + A = te.placeholder((N, L), name="A", dtype=dtype) + B = te.placeholder((L, M), name="B", dtype=dtype) + + k = te.reduce_axis((0, L), name="k") + C = te.compute((N, M), lambda i, j: te.sum(A[i, k] * B[k, j], axis=k), name="C") + s = te.create_schedule(C.op) + + # 调度 + y, x = s[C].op.axis + k = s[C].op.reduce_axis[0] + + # 2. 获取 config 对象 + cfg = autotvm.get_config() + + # 3. 定义搜索空间 + cfg.define_knob("tile_y", [1, 2, 4, 8, 16]) + cfg.define_knob("tile_x", [1, 2, 4, 8, 16]) + + # 4. 根据 config 进行调度 + yo, yi = s[C].split(y, cfg["tile_y"].val) + xo, xi = s[C].split(x, cfg["tile_x"].val) + + s[C].reorder(yo, xo, k, yi, xi) + + return s, [A, B, C] +``` + +下面将对前面的调度代码作出四个修改,然后得到一个可调的“模板”。一一解释这些修改: + +1. 使用装饰器将此函数标记为简单模板。 +2. 获取 config 对象:将 `cfg` 视为此函数的参数,但我们以另外的方式获取它。cfg 参数使得这个函数不再是一个确定的 schedule。将不同的配置传递给这个函数,可以得到不同的 schedule。这种使用配置对象的函数称为“模板”。 + + 为使模板函数更精炼,可在单个函数中定义参数搜索空间: + 1. 用一组值来定义搜索空间。将 `cfg` 转为 `ConfigSpace` 对象,收集此函数中的所有可调 knob,然后从中构建一个搜索空间。 + 2. 根据空间中的实体进行调度。将 `cfg` 转为 `ConfigEntity` 对象,当它被转为 `ConfigEntity` 后,会忽略所有空间定义 API(即 `cfg.define_XXXXX(...)`),但会存储所有可调 knob 的确定值,并根据这些值进行调度。 + + 在 auto-tuning 的过程中,首先用 `ConfigSpace` 对象调用这个模板来构建搜索空间,然后在构建的空间中用不同的 `ConfigEntity` 调用这个模板,来得到不同的 schedule。最后,我们将评估由不同 schedule 生成的代码,然后选择最佳的 schedule。 +4. 定义两个可调 knob。第一个是 `tile_y`,它有 5 个可能值。第二个是 `tile_x`,它和前者具有相同的可能值。这两个 knob 是独立的,所以它们跨越大小为 25 = 5x5 的搜索空间。 +5. 配置 knob 被传递给 `split` 调度操作,然后可以根据之前在 `cfg` 中定义的 5x5 确定值进行调度。 + +## 带有高级参数 API 的矩阵乘法模板 + +前面的模板手动列出了 konb 的所有可能值,它是用来定义空间的最底层 API,显示列出了要搜索的参数空间。这里推荐使用另一组更高级的 API,它可以更简单、更智能地定义搜索空间。 + +下面的示例用 `ConfigSpace.define_split` 来定义拆分 knob。它列举了所有可能的拆分 axis 和构造空间的方法。 + +同时,`ConfigSpace.define_reorder` 用于对 knob 重新排序,`ConfigSpace.define_annotate` 用于对展开、向量化、线程绑定等进行注释 。当高级 API 无法满足你的需求时,可以回退使用底层 API。 + +``` python +@autotvm.template("tutorial/matmul") +def matmul(N, L, M, dtype): + A = te.placeholder((N, L), name="A", dtype=dtype) + B = te.placeholder((L, M), name="B", dtype=dtype) + + k = te.reduce_axis((0, L), name="k") + C = te.compute((N, M), lambda i, j: te.sum(A[i, k] * B[k, j], axis=k), name="C") + s = te.create_schedule(C.op) + + # 调度 + y, x = s[C].op.axis + k = s[C].op.reduce_axis[0] + + ##### 开始定义空间 ##### + cfg = autotvm.get_config() + cfg.define_split("tile_y", y, num_outputs=2) + cfg.define_split("tile_x", x, num_outputs=2) + ##### 结束定义空间 ##### + + # 根据 config 进行调度 + yo, yi = cfg["tile_y"].apply(s, C, y) + xo, xi = cfg["tile_x"].apply(s, C, x) + + s[C].reorder(yo, xo, k, yi, xi) + + return s, [A, B, C] +``` + +:::note 关于 `cfg.define_split` 的更多解释 +在此模板中,`cfg.define_split("tile_y", y, num_outputs=2)` 枚举了所有可能的组合(以 y 的长度为因子,将 y 轴分成两个轴)。例如,如果 y 的长度为 32 并且想以 32 为因子将它拆分为两个轴,那么(外轴长度,内轴长度)有 6 个可能的值,即 (32, 1),(16, 2),(8, 4),(4, 8),(2, 16) 或 (1, 32)。这些也是 *tile_y* 的 6 个可能值。 + +调度过程中,`cfg["tile_y"]` 是一个 `SplitEntity` 对象。我们将外轴和内轴的长度存储在 `cfg['tile_y'].size` (有两个元素的元组)中。这个模板使用 `yo, yi = cfg['tile_y'].apply(s, C, y)` 来应用它。其实等价于 `yo, yi = s[C].split(y, cfg["tile_y"].size[1])` 或 `yo, yi = s[C].split(y, nparts=cfg['tile_y"].size[0])`。 + +cfg.apply API 的优点是它使多级拆分(即当 num_outputs >= 3 时)变得更加简单。 +::: + +## 第 2 步:使用 AutoTVM 优化矩阵乘法 + +第 1 步编写的矩阵乘法模板,可对拆分的 schedule 中的块大小进行参数化。通过第 1 步,可以实现对这个参数空间进行搜索。下一步是选择一个调优器来指导如何对空间进行探索。 + +### TVM 的自动调优器 + +调优器的任务可用以下伪代码来描述: + +``` python +ct = 0 +while ct < max_number_of_trials: + propose a batch of configs + measure this batch of configs on real hardware and get results + ct += batch_size +``` + +调优器可采取不同的策略来计划下一批配置,包括: + +* `tvm.autotvm.tuner.RandomTuner` :以随机顺序枚举空间 +* `tvm.autotvm.tuner.GridSearchTuner` :以网格搜索顺序枚举空间 +* `tvm.autotvm.tuner.GATuner` :使用遗传算法搜索空间 +* `tvm.autotvm.tuner.XGBTuner` :用基于模型的方法训练一个 XGBoost 模型,来预测降级 IR 的速度,并根据预测值选择下一批配置。 + +可根据空间大小、时间预算和其他因素来选择调优器。例如,如果你的空间非常小(小于 1000),则网格搜索调优器或随机调优器就够了。如果你的空间在 10^9 级别(CUDA GPU 上的 conv2d 算子的空间大小),XGBoostTuner 可以更有效地探索并找到更好的配置。 + +### 开始调优 + +下面继续矩阵乘法的示例。首先创建一个调优任务,然后检查初始的搜索空间。下面示例中是 512x512 的矩阵乘法,空间大小为 10x10=100。注意,任务和搜索空间与选择的调优器无关。 + +``` python +N, L, M = 512, 512, 512 +task = autotvm.task.create("tutorial/matmul", args=(N, L, M, "float32"), target="llvm") +print(task.config_space) +``` + +输出结果: + +``` bash +ConfigSpace (len=100, space_map= + 0 tile_y: Split(policy=factors, product=512, num_outputs=2) len=10 + 1 tile_x: Split(policy=factors, product=512, num_outputs=2) len=10 +) +``` + +然后定义如何评估生成的代码,并且选择一个调优器。由于我们的空间很小,所以随机调优器就可以。 + +本教程只做 10 次试验进行演示。实际上可以根据自己的时间预算进行更多试验。调优结果会记录到日志文件中。这个文件可用于选择之后发现的调优器的最佳配置。 + +``` python +# 记录 config(为了将 tuning 日志打印到屏幕) +logging.getLogger("autotvm").setLevel(logging.DEBUG) +logging.getLogger("autotvm").addHandler(logging.StreamHandler(sys.stdout)) +``` + +评估配置有两个步骤:构建和运行。默认用所有 CPU core 来编译程序。然后依次进行评估。为了减少方差,对 5 次评估结果取平均值。 + +``` python +measure_option = autotvm.measure_option(builder="local", runner=autotvm.LocalRunner(number=5)) + +# 用 RandomTuner 开始调优, 日志记录到 `matmul.log` 文件中 +# 可用 XGBTuner 来替代. +tuner = autotvm.tuner.RandomTuner(task) +tuner.tune( + n_trial=10, + measure_option=measure_option, + callbacks=[autotvm.callback.log_to_file("matmul.log")], +) +``` + +输出结果: + +``` bash +waiting for device... +device available +Get devices for measurement successfully! +No: 1 GFLOPS: 8.48/8.48 result: MeasureResult(costs=(0.0316434228,), error_no=MeasureErrorNo.NO_ERROR, all_cost=0.638512134552002, timestamp=1657225928.6342561) [('tile_y', [-1, 1]), ('tile_x', [-1, 256])],None,80 +No: 2 GFLOPS: 2.30/8.48 result: MeasureResult(costs=(0.1165478966,), error_no=MeasureErrorNo.NO_ERROR, all_cost=2.0105199813842773, timestamp=1657225930.6636436) [('tile_y', [-1, 4]), ('tile_x', [-1, 8])],None,32 +No: 3 GFLOPS: 11.82/11.82 result: MeasureResult(costs=(0.0227097348,), error_no=MeasureErrorNo.NO_ERROR, all_cost=0.5589795112609863, timestamp=1657225931.7059512) [('tile_y', [-1, 64]), ('tile_x', [-1, 32])],None,56 +No: 4 GFLOPS: 1.66/11.82 result: MeasureResult(costs=(0.1616202114,), error_no=MeasureErrorNo.NO_ERROR, all_cost=2.6911513805389404, timestamp=1657225934.9635096) [('tile_y', [-1, 1]), ('tile_x', [-1, 4])],None,20 +No: 5 GFLOPS: 3.65/11.82 result: MeasureResult(costs=(0.073561817,), error_no=MeasureErrorNo.NO_ERROR, all_cost=1.3051848411560059, timestamp=1657225936.3988533) [('tile_y', [-1, 256]), ('tile_x', [-1, 16])],None,48 +No: 6 GFLOPS: 1.85/11.82 result: MeasureResult(costs=(0.1452834464,), error_no=MeasureErrorNo.NO_ERROR, all_cost=2.5179028511047363, timestamp=1657225938.961955) [('tile_y', [-1, 512]), ('tile_x', [-1, 4])],None,29 +No: 7 GFLOPS: 0.87/11.82 result: MeasureResult(costs=(0.30933780240000003,), error_no=MeasureErrorNo.NO_ERROR, all_cost=5.067087888717651, timestamp=1657225944.589149) [('tile_y', [-1, 512]), ('tile_x', [-1, 2])],None,19 +No: 8 GFLOPS: 10.53/11.82 result: MeasureResult(costs=(0.025489421,), error_no=MeasureErrorNo.NO_ERROR, all_cost=0.5452830791473389, timestamp=1657225945.1592515) [('tile_y', [-1, 4]), ('tile_x', [-1, 64])],None,62 +No: 9 GFLOPS: 1.58/11.82 result: MeasureResult(costs=(0.16960762680000002,), error_no=MeasureErrorNo.NO_ERROR, all_cost=2.8109781742095947, timestamp=1657225948.0900776) [('tile_y', [-1, 2]), ('tile_x', [-1, 2])],None,11 +No: 10 GFLOPS: 2.42/11.82 result: MeasureResult(costs=(0.11083148779999999,), error_no=MeasureErrorNo.NO_ERROR, all_cost=1.8757600784301758, timestamp=1657225950.0266354) [('tile_y', [-1, 4]), ('tile_x', [-1, 4])],None,22 +``` + +调优完成后,可从日志文件中选择具有最佳评估性能的配置,并用相应参数来编译 schedule。快速验证 schedule 是否产生了正确的结果,可直接在 `autotvm.apply_history_best` 上下文中调用 `matmul` 函数,它会用参数查询调度上下文,然后可用相同的参数获取最优配置。 + +``` python +# 从日志文件中应用历史最佳 +with autotvm.apply_history_best("matmul.log"): + with tvm.target.Target("llvm"): + s, arg_bufs = matmul(N, L, M, "float32") + func = tvm.build(s, arg_bufs) + +# 验证正确性 +a_np = np.random.uniform(size=(N, L)).astype(np.float32) +b_np = np.random.uniform(size=(L, M)).astype(np.float32) +c_np = a_np.dot(b_np) + +c_tvm = tvm.nd.empty(c_np.shape) +func(tvm.nd.array(a_np), tvm.nd.array(b_np), c_tvm) + +tvm.testing.assert_allclose(c_np, c_tvm.numpy(), rtol=1e-4) +``` + +输出结果: + +``` plain +Finish loading 10 records +``` + +## 总结 + +本教程展示了如何构建算子模板,使得 TVM 能够搜索参数空间,并选择优化的调度配置。为了更深入地了解其工作原理,推荐基于 *张量表达式入门 ``* 教程中演示的调度操作,向调度添加新的搜索参数。接下来的章节将演示 AutoScheduler,它是TVM 中一种优化常用算子的方法,同时无需用户提供自定义的模板。 + +[下载 Python 源代码:autotvm_matmul_x86.py](https://tvm.apache.org/docs/_downloads/8e7bbc9dbdda76ac573b24606b41c006/autotvm_matmul_x86.py) + +[下载 Jupyter Notebook:autotvm_matmul_x86.ipynb](https://tvm.apache.org/docs/_downloads/37bbf9e2065ec8deeb64a8d9fa0755bc/autotvm_matmul_x86.ipynb) diff --git a/versioned_docs/version-0.12.0/tutorial/08-ops_AutoScheduling.md b/versioned_docs/version-0.12.0/tutorial/08-ops_AutoScheduling.md new file mode 100644 index 00000000..2982e2bf --- /dev/null +++ b/versioned_docs/version-0.12.0/tutorial/08-ops_AutoScheduling.md @@ -0,0 +1,280 @@ +--- +title: 使用 Auto-scheduling 优化算子 +--- + +# 使用 Auto-scheduling 优化算子 + +:::note +单击 [此处](https://tvm.apache.org/docs/tutorial/auto_scheduler_matmul_x86.html#sphx-glr-download-tutorial-auto-scheduler-matmul-x86-py) 下载完整的示例代码 +::: + +**作者**:[Lianmin Zheng](https://github.com/merrymercy),[Chengfan Jia](https://github.com/jcf94/) + +本教程将展示 TVM 的 Auto Scheduling 功能,如何在不编写自定义模板的情况下,找到最佳 schedule。 + +与基于模板的 [AutoTVM](https://tvm.apache.org/docs/tutorial/autotvm_matmul_x86.html) 依赖手工模板来定义搜索空间不同,auto-scheduler 不需要任何模板。用户只需编写计算声明,无需任何 schedule 命令或模板。auto-scheduler 可以自动生成一个大的搜索空间,并在空间中找到最优 schedule。 + +本教程中使用矩阵乘法作为示例。 + +:::note +注意,本教程不会在 Windows 或最新版本的 macOS 上运行。如需运行,请将本教程的主体放在 `if __name__ == "__main__":` 代码块中。 +::: + +``` python +import os + +import numpy as np +import tvm +from tvm import te, auto_scheduler +``` + +## 定义矩阵乘法 + +首先,定义一个带有偏置加法的矩阵乘法。注意,这里使用了 TVM 张量表达式语言中的标准操作。主要区别在于函数定义上方使用了 `register_workload` 装饰器。该函数应返回输入/输出张量列表。通过这些张量,auto-scheduler 可以得到整个计算图。 + +``` python +@auto_scheduler.register_workload # Note the auto_scheduler decorator +def matmul_add(N, L, M, dtype): + A = te.placeholder((N, L), name="A", dtype=dtype) + B = te.placeholder((L, M), name="B", dtype=dtype) + C = te.placeholder((N, M), name="C", dtype=dtype) + + k = te.reduce_axis((0, L), name="k") + matmul = te.compute( + (N, M), + lambda i, j: te.sum(A[i, k] * B[k, j], axis=k), + name="matmul", + attrs={"layout_free_placeholders": [B]}, # enable automatic layout transform for tensor B + ) + out = te.compute((N, M), lambda i, j: matmul[i, j] + C[i, j], name="out") + + return [A, B, C, out] +``` + +## 创建搜索任务 + +定义函数后,可以为 auto_scheduler 创建要搜索的任务。我们为这个矩阵乘法指定了特定的参数,如这里是两个大小为 1024x1024 的矩阵乘法。然后我们创建一个 N=L=M=1024 和 dtype="float32" 的搜索任务 + +:::note 使用自定义 target 提高性能 +为让 TVM 充分利用特定的硬件平台,需要手动指定 CPU 功能。例如: + +* 启用 AVX2:将下面的 `llvm` 替换为 `llvm -mcpu=core-avx2` +* 启用 AVX-512:将下面的 `llvm` 替换为 `llvm -mcpu=skylake-avx512` +::: + +``` python +target = tvm.target.Target("llvm") +N = L = M = 1024 +task = tvm.auto_scheduler.SearchTask(func=matmul_add, args=(N, L, M, "float32"), target=target) + +# 检查计算图 +print("Computational DAG:") +print(task.compute_dag) +``` + +输出结果: + +``` bash +Computational DAG: +A = PLACEHOLDER [1024, 1024] +B = PLACEHOLDER [1024, 1024] +matmul(i, j) += (A[i, k]*B[k, j]) +C = PLACEHOLDER [1024, 1024] +out(i, j) = (matmul[i, j] + C[i, j]) +``` + +## 设置 auto-scheduler 的参数 + +接下来,为 auto-scheduler 设置参数。 + +* `num_measure_trials` 表示搜索过程中可用的测试试验次数。本教程为了快速演示只进行了 10 次试验。实际上,1000 是搜索收敛的最佳数量。可以根据自己的时间预算进行更多试验。 +* 此外,我们用 `RecordToFile` 将测试记录记录到文件 `matmul.json` 中。测试记录可用于查询历史最佳、恢复搜索以及以后进行更多分析。 +* 有关更多参数,参见 `TuningOptions` + +``` python +log_file = "matmul.json" +tune_option = auto_scheduler.TuningOptions( + num_measure_trials=10, + measure_callbacks=[auto_scheduler.RecordToFile(log_file)], + verbose=2, +) +``` + +## 开始搜索 + +准备好所有输入就可以开始搜索,让 auto-scheduler 发挥它的作用。经过一些测试试验后,可从日志文件中加载最佳 schedule 并应用。 + +``` python +# 运行 auto-tuning(搜索) +task.tune(tune_option) +# 应用最佳 schedule +sch, args = task.apply_best(log_file) +``` + +## 检查优化的 schedule + +auto-scheduling 完成后,可将 schedule 降级来查看 IR。auto-scheduler 执行合适的优化,包括多级循环切分、布局转换、并行化、向量化、循环展开和算子融合。 + +``` python +print("Lowered TIR:") +print(tvm.lower(sch, args, simple_mode=True)) +``` + +输出结果: + +``` bash +Lowered TIR: +@main = primfn(A_1: handle, B_1: handle, C_1: handle, out_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []), + B: Buffer(B_2: Pointer(float32), float32, [1048576], []), + C: Buffer(C_2: Pointer(float32), float32, [1048576], []), + out: Buffer(out_2: Pointer(float32), float32, [1048576], [])} + buffer_map = {A_1: A, B_1: B, C_1: C, out_1: out} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [1024, 1024], []), B_1: B_3: Buffer(B_2, float32, [1024, 1024], []), C_1: C_3: Buffer(C_2, float32, [1024, 1024], []), out_1: out_3: Buffer(out_2, float32, [1024, 1024], [])} { + allocate(auto_scheduler_layout_transform: Pointer(global float32), float32, [1048576]), storage_scope = global { + for (ax0.ax1.fused.ax2.fused: int32, 0, 128) "parallel" { + for (ax4: int32, 0, 256) { + for (ax6: int32, 0, 4) { + for (ax7: int32, 0, 8) { + auto_scheduler_layout_transform_1: Buffer(auto_scheduler_layout_transform, float32, [1048576], [])[((((ax0.ax1.fused.ax2.fused*8192) + (ax4*32)) + (ax6*8)) + ax7)] = B[((((ax4*4096) + (ax6*1024)) + (ax0.ax1.fused.ax2.fused*8)) + ax7)] + } + } + } + } + for (i.outer.outer.j.outer.outer.fused: int32, 0, 16384) "parallel" { + allocate(matmul: Pointer(global float32x8), float32x8, [4]), storage_scope = global; + for (i.outer.inner: int32, 0, 2) { + matmul_1: Buffer(matmul, float32x8, [4], [])[0] = broadcast(0f32, 8) + matmul_1[1] = broadcast(0f32, 8) + matmul_1[2] = broadcast(0f32, 8) + matmul_1[3] = broadcast(0f32, 8) + for (k.outer: int32, 0, 256) { + for (k.inner: int32, 0, 4) { + let cse_var_2: int32 = (((floormod(i.outer.outer.j.outer.outer.fused, 128)*8192) + (k.outer*32)) + (k.inner*8)) + let cse_var_1: int32 = ((((floordiv(i.outer.outer.j.outer.outer.fused, 128)*8192) + (i.outer.inner*4096)) + (k.outer*4)) + k.inner) + { + matmul_1[0] = (matmul_1[0] + (broadcast(A[cse_var_1], 8)*auto_scheduler_layout_transform_1[ramp(cse_var_2, 1, 8)])) + matmul_1[1] = (matmul_1[1] + (broadcast(A[(cse_var_1 + 1024)], 8)*auto_scheduler_layout_transform_1[ramp(cse_var_2, 1, 8)])) + matmul_1[2] = (matmul_1[2] + (broadcast(A[(cse_var_1 + 2048)], 8)*auto_scheduler_layout_transform_1[ramp(cse_var_2, 1, 8)])) + matmul_1[3] = (matmul_1[3] + (broadcast(A[(cse_var_1 + 3072)], 8)*auto_scheduler_layout_transform_1[ramp(cse_var_2, 1, 8)])) + } + } + } + for (i.inner: int32, 0, 4) { + let cse_var_3: int32 = ((((floordiv(i.outer.outer.j.outer.outer.fused, 128)*8192) + (i.outer.inner*4096)) + (i.inner*1024)) + (floormod(i.outer.outer.j.outer.outer.fused, 128)*8)) + out[ramp(cse_var_3, 1, 8)] = (matmul_1[i.inner] + C[ramp(cse_var_3, 1, 8)]) + } + } + } + } +} +``` + +## 检查正确性并评估性能 + +构建二进制文件并检查其正确性和性能。 + +``` python +func = tvm.build(sch, args, target) +a_np = np.random.uniform(size=(N, L)).astype(np.float32) +b_np = np.random.uniform(size=(L, M)).astype(np.float32) +c_np = np.random.uniform(size=(N, M)).astype(np.float32) +out_np = a_np.dot(b_np) + c_np + +dev = tvm.cpu() +a_tvm = tvm.nd.array(a_np, device=dev) +b_tvm = tvm.nd.array(b_np, device=dev) +c_tvm = tvm.nd.array(c_np, device=dev) +out_tvm = tvm.nd.empty(out_np.shape, device=dev) +func(a_tvm, b_tvm, c_tvm, out_tvm) + +# Check results +np.testing.assert_allclose(out_np, out_tvm.numpy(), rtol=1e-3) + +# Evaluate execution time. +evaluator = func.time_evaluator(func.entry_name, dev, min_repeat_ms=500) +print( + "Execution time of this operator: %.3f ms" + % (np.median(evaluator(a_tvm, b_tvm, c_tvm, out_tvm).results) * 1000) +) +``` + +输出结果: + +``` bash +Execution time of this operator: 93.286 ms +``` + +## 使用记录文件 + +在搜索过程中,所有测试记录都保存到记录文件 `matmul.json` 中。测试记录可用于重新应用搜索结果、恢复搜索和执行其他分析。 + +下面是从文件中加载最佳 schedule,并打印等效的 Python schedule API 的例子。可用于调试和学习 auto-scheduler 的行为。 + +``` python +print("Equivalent python schedule:") +print(task.print_best(log_file)) +``` + +输出结果: + +``` bash +Equivalent python schedule: +matmul_i, matmul_j, matmul_k = tuple(matmul.op.axis) + tuple(matmul.op.reduce_axis) +out_i, out_j = tuple(out.op.axis) + tuple(out.op.reduce_axis) +matmul_i_o_i, matmul_i_i = s[matmul].split(matmul_i, factor=4) +matmul_i_o_o_i, matmul_i_o_i = s[matmul].split(matmul_i_o_i, factor=1) +matmul_i_o_o_o, matmul_i_o_o_i = s[matmul].split(matmul_i_o_o_i, factor=2) +matmul_j_o_i, matmul_j_i = s[matmul].split(matmul_j, factor=8) +matmul_j_o_o_i, matmul_j_o_i = s[matmul].split(matmul_j_o_i, factor=1) +matmul_j_o_o_o, matmul_j_o_o_i = s[matmul].split(matmul_j_o_o_i, factor=1) +matmul_k_o, matmul_k_i = s[matmul].split(matmul_k, factor=4) +s[matmul].reorder(matmul_i_o_o_o, matmul_j_o_o_o, matmul_i_o_o_i, matmul_j_o_o_i, matmul_k_o, matmul_i_o_i, matmul_j_o_i, matmul_k_i, matmul_i_i, matmul_j_i) +out_i_o_i, out_i_i = s[out].split(out_i, factor=4) +out_i_o_o, out_i_o_i = s[out].split(out_i_o_i, factor=2) +out_j_o_i, out_j_i = s[out].split(out_j, factor=8) +out_j_o_o, out_j_o_i = s[out].split(out_j_o_i, factor=1) +s[out].reorder(out_i_o_o, out_j_o_o, out_i_o_i, out_j_o_i, out_i_i, out_j_i) +s[matmul].compute_at(s[out], out_j_o_i) +out_i_o_o_j_o_o_fused = s[out].fuse(out_i_o_o, out_j_o_o) +s[out].parallel(out_i_o_o_j_o_o_fused) +s[matmul].pragma(matmul_i_o_o_o, "auto_unroll_max_step", 8) +s[matmul].pragma(matmul_i_o_o_o, "unroll_explicit", True) +s[matmul].vectorize(matmul_j_i) +s[out].vectorize(out_j_i) +``` + +恢复搜索则更为复杂,需要自己创建搜索策略和 cost model,并通过日志文件恢复搜索策略和 cost model 的状态。下面的示例进行了 5 次试验来恢复它们的状态: + +``` python +def resume_search(task, log_file): + print("Resume search:") + cost_model = auto_scheduler.XGBModel() + cost_model.update_from_file(log_file) + search_policy = auto_scheduler.SketchPolicy( + task, cost_model, init_search_callbacks=[auto_scheduler.PreloadMeasuredStates(log_file)] + ) + tune_option = auto_scheduler.TuningOptions( + num_measure_trials=5, measure_callbacks=[auto_scheduler.RecordToFile(log_file)] + ) + task.tune(tune_option, search_policy=search_policy) + +resume_search(task, log_file) +``` + +输出结果: + +``` bash +Resume search: +/usr/local/lib/python3.7/dist-packages/xgboost/training.py:17: UserWarning: Old style callback is deprecated. See: https://xgboost.readthedocs.io/en/latest/python/callbacks.html + warnings.warn(f'Old style callback is deprecated. See: {link}', UserWarning) +``` + +## 最后的说明和总结 + +本教程展示了如何在不指定搜索模板的情况下,使用 TVM Auto-Scheduler 自动优化矩阵乘法。从张量表达式(TE)语言开始,演示了一系列关于 TVM 如何优化计算操作的示例。 + +[下载 Python 源代码:auto_scheduler_matmul_x86.py](https://tvm.apache.org/docs/_downloads/eac4389b114db015e95cb3cdf8b86b83/auto_scheduler_matmul_x86.py) + +[下载 Jupyter Notebook:auto_scheduler_matmul_x86.ipynb](https://tvm.apache.org/docs/_downloads/246d4b8509474fd9046e69f6cc9b7f87/auto_scheduler_matmul_x86.ipynb) diff --git a/versioned_docs/version-0.12.0/tutorial/09-tensorIR.md b/versioned_docs/version-0.12.0/tutorial/09-tensorIR.md new file mode 100644 index 00000000..1d8a8d4d --- /dev/null +++ b/versioned_docs/version-0.12.0/tutorial/09-tensorIR.md @@ -0,0 +1,287 @@ +--- +title: TensorIR 快速入门 +--- + +# TensorIR 快速入门 + +:::note +单击 [此处](https://tvm.apache.org/docs/tutorial/tensor_ir_blitz_course.html#sphx-glr-download-tutorial-tensor-ir-blitz-course-py) 下载完整的示例代码 +::: + +**作者**:[Siyuan Feng](https://github.com/Hzfengsy) + +TensorIR 是深度学习领域的特定语言,主要有两个作用: + +* 在各种硬件后端转换和优化程序。 +* 自动 \_tensorized\_ 程序优化的抽象。 + +``` python +import tvm +from tvm.ir.module import IRModule +from tvm.script import tir as T +import numpy as np +``` + +## IRModule + +IRModule 是 TVM 的核心数据结构,它包含深度学习程序,并且是 IR 转换和模型构建的基础。 + + ![/img/docs/tlc-pack/web-data/main/images/design/tvm_life_of_irmodule.png](/img/docs/tlc-pack/web-data/main/images/design/tvm_life_of_irmodule.png) + +上图展示的是 IRModule 的生命周期,它可由 TVMScript 创建。转换 IRModule 的两种主要方法是 TensorIR 的 schedule 原语转换和 pass 转换。此外,也可直接对 IRModule 进行一系列转换。注意,可以在**任何**阶段将 IRModule 打印到 TVMScript。完成所有转换和优化后,可将 IRModule 构建为可运行模块,从而部署在目标设备上。 + +基于 TensorIR 和 IRModule 的设计,可创建一种新的编程方法: + +1. 基于 Python-AST 语法,用 TVMScript 编写程序。 +2. 使用 Python API 转换和优化程序。 +3. 使用命令式转换 API 交互检查和提高性能。 + +## 创建 IRModule + +IRModule 是 TVM IR 的一种可往返语法,可通过编写 TVMScript 来创建。 + +与通过张量表达式创建计算表达式([使用张量表达式操作算子](https://tvm.apache.org/docs/tutorial/tensor_expr_get_started.html#tutorial-tensor-expr-get-started))不同,TensorIR 允许用户通过 TVMScript(一种嵌在 Python AST 中的语言)进行编程。新方法可以编写复杂的程序并进一步调度和优化。 + +下面是向量加法的示例: + +``` python +@tvm.script.ir_module +class MyModule: + @T.prim_func + def main(a: T.handle, b: T.handle): + # 我们通过 T.handle 进行数据交换,类似于内存指针 + T.func_attr({"global_symbol": "main", "tir.noalias": True}) + # 通过 handle 创建 Buffer + A = T.match_buffer(a, (8,), dtype="float32") + B = T.match_buffer(b, (8,), dtype="float32") + for i in range(8): + # block 是针对计算的抽象 + with T.block("B"): + # 定义一个空间(可并行)block 迭代器,并且将它的值绑定成 i + vi = T.axis.spatial(8, i) + B[vi] = A[vi] + 1.0 + +ir_module = MyModule +print(type(ir_module)) +print(ir_module.script()) +``` + +输出结果: + +``` bash + +# from tvm.script import tir as T +@tvm.script.ir_module +class Module: + @T.prim_func + def main(A: T.Buffer[8, "float32"], B: T.Buffer[8, "float32"]) -> None: + # function attr dict + T.func_attr({"global_symbol": "main", "tir.noalias": True}) + # body + # with T.block("root") + for i in T.serial(8): + with T.block("B"): + vi = T.axis.spatial(8, i) + T.reads(A[vi]) + T.writes(B[vi]) + B[vi] = A[vi] + T.float32(1) +``` + +此外,我们还可以使用张量表达式 DSL 编写简单的运算符,并将它们转换为 IRModule。 + +``` python +from tvm import te + +A = te.placeholder((8,), dtype="float32", name="A") +B = te.compute((8,), lambda *i: A(*i) + 1.0, name="B") +func = te.create_prim_func([A, B]) +ir_module_from_te = IRModule({"main": func}) +print(ir_module_from_te.script()) +``` + +输出结果: + +``` bash +# from tvm.script import tir as T +@tvm.script.ir_module +class Module: + @T.prim_func + def main(A: T.Buffer[8, "float32"], B: T.Buffer[8, "float32"]) -> None: + # function attr dict + T.func_attr({"global_symbol": "main", "tir.noalias": True}) + # body + # with T.block("root") + for i0 in T.serial(8): + with T.block("B"): + i0_1 = T.axis.spatial(8, i0) + T.reads(A[i0_1]) + T.writes(B[i0_1]) + B[i0_1] = A[i0_1] + T.float32(1) +``` + +## 构建并运行 IRModule + +可将 IRModule 构建为特定 target 后端的可运行模块。 + +``` python +mod = tvm.build(ir_module, target="llvm") # CPU 后端的模块 +print(type(mod)) +``` + +输出结果: + +``` bash + +``` + +准备输入数组和输出数组,然后运行模块: + +``` python +a = tvm.nd.array(np.arange(8).astype("float32")) +b = tvm.nd.array(np.zeros((8,)).astype("float32")) +mod(a, b) +print(a) +print(b) +``` + +输出结果: + +``` bash +[0. 1. 2. 3. 4. 5. 6. 7.] +[1. 2. 3. 4. 5. 6. 7. 8.] +``` + +## 转换 IRModule + +IRModule 是程序优化的核心数据结构,可通过 `Schedule` 进行转换。schedule 包含多个 primitive 方法来交互地转换程序。每个 primitive 都以特定方式对程序进行转换,从而优化性能。 + + ![/img/docs/tlc-pack/web-data/main/images/design/tvm_tensor_ir_opt_flow.png](/img/docs/tlc-pack/web-data/main/images/design/tvm_tensor_ir_opt_flow.png) + +上图是优化张量程序的典型工作流程。首先,用 TVMScript 或张量表达式创建一个初始 IRModule,然后在这个初始 IRModule 上创建 schedule。接下来,使用一系列调度原语来提高性能。最后,我们可以将其降低并构建成一个可运行模块。 + +上面只演示了一个简单的转换。首先,在输入 *ir_module* 上创建 schedule: + +``` python +sch = tvm.tir.Schedule(ir_module) +print(type(sch)) +``` + +输出结果: + +``` bash + +``` + +将嵌套循环展开成 3 个循环,并打印结果: + +``` python +# 通过名字获取 block +block_b = sch.get_block("B") +# 获取包围 block 的循环 +(i,) = sch.get_loops(block_b) +# 展开嵌套循环 +i_0, i_1, i_2 = sch.split(i, factors=[2, 2, 2]) +print(sch.mod.script()) +``` + +输出结果: + +``` bash +# from tvm.script import tir as T +@tvm.script.ir_module +class Module: + @T.prim_func + def main(A: T.Buffer[8, "float32"], B: T.Buffer[8, "float32"]) -> None: + # function attr dict + T.func_attr({"global_symbol": "main", "tir.noalias": True}) + # body + # with T.block("root") + for i_0, i_1, i_2 in T.grid(2, 2, 2): + with T.block("B"): + vi = T.axis.spatial(8, i_0 * 4 + i_1 * 2 + i_2) + T.reads(A[vi]) + T.writes(B[vi]) + B[vi] = A[vi] + T.float32(1) +``` + +还可对循环重新排序。例如,将循环 *i_2* 移到 *i_1* 之外: + +``` python +sch.reorder(i_0, i_2, i_1) +print(sch.mod.script()) +``` + +输出结果: + +``` bash +# from tvm.script import tir as T +@tvm.script.ir_module +class Module: + @T.prim_func + def main(A: T.Buffer[8, "float32"], B: T.Buffer[8, "float32"]) -> None: + # function attr dict + T.func_attr({"global_symbol": "main", "tir.noalias": True}) + # body + # with T.block("root") + for i_0, i_2, i_1 in T.grid(2, 2, 2): + with T.block("B"): + vi = T.axis.spatial(8, i_0 * 4 + i_1 * 2 + i_2) + T.reads(A[vi]) + T.writes(B[vi]) + B[vi] = A[vi] + T.float32(1) +``` + +### 转换为 GPU 程序 + +要在 GPU 上部署模型必须进行线程绑定。幸运的是,也可以用原语来增量转换。 + +``` python +sch.bind(i_0, "blockIdx.x") +sch.bind(i_2, "threadIdx.x") +print(sch.mod.script()) +``` + +输出结果: + +``` bash +# from tvm.script import tir as T +@tvm.script.ir_module +class Module: + @T.prim_func + def main(A: T.Buffer[8, "float32"], B: T.Buffer[8, "float32"]) -> None: + # function attr dict + T.func_attr({"global_symbol": "main", "tir.noalias": True}) + # body + # with T.block("root") + for i_0 in T.thread_binding(2, thread="blockIdx.x"): + for i_2 in T.thread_binding(2, thread="threadIdx.x"): + for i_1 in T.serial(2): + with T.block("B"): + vi = T.axis.spatial(8, i_0 * 4 + i_1 * 2 + i_2) + T.reads(A[vi]) + T.writes(B[vi]) + B[vi] = A[vi] + T.float32(1) +``` + +绑定线程后,用 `cuda` 后端来构建 IRModule: + +``` python +ctx = tvm.cuda(0) +cuda_mod = tvm.build(sch.mod, target="cuda") +cuda_a = tvm.nd.array(np.arange(8).astype("float32"), ctx) +cuda_b = tvm.nd.array(np.zeros((8,)).astype("float32"), ctx) +cuda_mod(cuda_a, cuda_b) +print(cuda_a) +print(cuda_b) +``` + +输出结果: + +``` bash +[0. 1. 2. 3. 4. 5. 6. 7.] +[1. 2. 3. 4. 5. 6. 7. 8.] +``` + +[下载 Python 源代码:tensor_ir_blitz_course.py](https://tvm.apache.org/docs/_downloads/5c7000b5aef924e29ec975ec3002ea03/tensor_ir_blitz_course.py) + +[下载 Jupyter Notebook:tensor_ir_blitz_course.ipynb](https://tvm.apache.org/docs/_downloads/c9bb7875c6ca5b2da162e177d3c9aac0/tensor_ir_blitz_course.ipynb) diff --git a/versioned_docs/version-0.12.0/tutorial/10-rpc.md b/versioned_docs/version-0.12.0/tutorial/10-rpc.md new file mode 100644 index 00000000..8c7b659a --- /dev/null +++ b/versioned_docs/version-0.12.0/tutorial/10-rpc.md @@ -0,0 +1,218 @@ +--- +title: 交叉编译和 RPC +--- + +# 交叉编译和 RPC + +:::note +单击 [此处](https://tvm.apache.org/docs/tutorial/cross_compilation_and_rpc.html#sphx-glr-download-tutorial-cross-compilation-and-rpc-py) 下载完整的示例代码 +::: + +**作者**:[Ziheng Jiang](https://github.com/ZihengJiang/),[Lianmin Zheng](https://github.com/merrymercy/) + +本教程介绍了如何在 TVM 中使用 RPC 进行交叉编译和远程设备执行。 + +利用交叉编译和 RPC,可以实现程序在本地机器编译,在远程设备运行。这个特性在远程设备资源有限时(如在树莓派和移动平台上)尤其有用。本教程将把树莓派作为 CPU 示例,把 Firefly-RK3399 作为 OpenCL 示例进行演示。 + +## 在设备上构建 TVM Runtime + +首先在远程设备上构建 TVM runtime。 + +:::note 注意 +本节和下一节中的所有命令都应在目标设备(例如树莓派)上执行。假设目标设备运行 Linux 系统。 +::: + +由于在本地机器上只做编译,而远程设备用于运行生成的代码。所以只需在远程设备上构建 TVM runtime。 + +``` bash +git clone --recursive https://github.com/apache/tvm tvm +cd tvm +make runtime -j2 +``` + +成功构建 runtime 后,要在 `~/.bashrc` 文件中设置环境变量。可以用 `vi ~/.bashrc`命令编辑 `~/.bashrc`,在这个文件里添加下面这行代码(假设 TVM 目录在 `~/tvm` 中): + +``` bash +export PYTHONPATH=$PYTHONPATH:~/tvm/python +``` + +执行 `source ~/.bashrc` 来更新环境变量。 + +## 在设备上设置 RPC 服务器 + +在远程设备(本例为树莓派)上运行以下命令来启动 RPC 服务器: + +``` bash +python -m tvm.exec.rpc_server --host 0.0.0.0 --port=9090 +``` + +看到下面这行提示,则表示 RPC 服务器已成功启动。 + +``` bash +INFO:root:RPCServer: bind to 0.0.0.0:9090 +``` + +## 在本地机器上声明和交叉编译内核 + +:::note +现在回到本地机器(已经用 LLVM 安装了完整的 TVM)。 +::: + +在本地机器上声明一个简单的内核: + +``` python +import numpy as np + +import tvm +from tvm import te +from tvm import rpc +from tvm.contrib import utils + +n = tvm.runtime.convert(1024) +A = te.placeholder((n,), name="A") +B = te.compute((n,), lambda i: A[i] + 1.0, name="B") +s = te.create_schedule(B.op) +``` + +然后交叉编译内核。对于树莓派 3B,target 是“llvm -mtriple=armv7l-linux-gnueabihf”,但这里用的是“llvm”,使得本教程可以在网页构建服务器上运行。请参阅下面的详细说明。 + +``` python +local_demo = True + +if local_demo: + target = "llvm" +else: + target = "llvm -mtriple=armv7l-linux-gnueabihf" + +func = tvm.build(s, [A, B], target=target, name="add_one") +# 将 lib 存储在本地临时文件夹 +temp = utils.tempdir() +path = temp.relpath("lib.tar") +func.export_library(path) +``` + +:::note +要使本教程运行在真正的远程设备上,需要将 `local_demo` 改为 False,并将 `build` 中的 `target` 替换为适合设备的 target 三元组。不同设备的 target 三元组可能不同。例如,对于树莓派 3B,它是 `llvm -mtriple=armv7l-linux-gnueabihf`;对于 RK3399,它是 `llvm -mtriple=aarch64-linux-gnu`。 + +通常,可以在设备上运行 `gcc -v` 来查询 target,寻找以 `Target` 开头的行:(尽管它可能仍然是一个松散的配置。) + +除了 `-mtriple`,还可设置其他编译选项,例如: + +* **`-mcpu=`** + + 指定生成的代码运行的芯片架构。默认情况这是从 target 三元组推断出来的,并自动检测到当前架构。 + +* **`-mattr=a1,+a2,-a3,…`** + + 覆盖或控制 target 的指定属性,例如是否启用 SIMD 操作。默认属性集由当前 CPU 设置。要获取可用属性列表,执行: + +``` bash + llc -mtriple= -mattr=help +``` + +这些选项与 [llc](http://llvm.org/docs/CommandGuide/llc.html) 一致。建议设置 target 三元组和功能集,使其包含可用的特定功能,这样我们可以充分利用单板的功能。查看 [LLVM 交叉编译指南](https://clang.llvm.org/docs/CrossCompilation.html) 获取有关交叉编译属性的详细信息。 +::: + +## 通过 RPC 远程运行 CPU 内核 + +下面将演示如何在远程设备上运行生成的 CPU 内核。首先,从远程设备获取 RPC 会话: + +``` python +if local_demo: + remote = rpc.LocalSession() +else: + # 下面是我的环境,将这个换成你目标设备的 IP 地址 + host = "10.77.1.162" + port = 9090 + remote = rpc.connect(host, port) +``` + +将 lib 上传到远程设备,然后调用设备的本地编译器重新链接它们。其中 *func* 是一个远程模块对象。 + +``` python +remote.upload(path) +func = remote.load_module("lib.tar") + +# 在远程设备上创建数组 +dev = remote.cpu() +a = tvm.nd.array(np.random.uniform(size=1024).astype(A.dtype), dev) +b = tvm.nd.array(np.zeros(1024, dtype=A.dtype), dev) +# 这个函数将在远程设备上运行 +func(a, b) +np.testing.assert_equal(b.numpy(), a.numpy() + 1) +``` + +要想评估内核在远程设备上的性能,避免网络开销很重要。`time_evaluator` 返回一个远程函数,这个远程函数多次运行 func 函数,并测试每一次在远程设备上运行的成本,然后返回测试的成本(不包括网络开销)。 + +``` python +time_f = func.time_evaluator(func.entry_name, dev, number=10) +cost = time_f(a, b).mean +print("%g secs/op" % cost) +``` + +输出结果: + +``` bash +1.369e-07 secs/op +``` + +## 通过 RPC 远程运行 OpenCL 内核 + +远程 OpenCL 设备的工作流程与上述内容基本相同。可以定义内核、上传文件,然后通过 RPC 运行。 + +:::note +树莓派不支持 OpenCL,下面的代码是在 Firefly-RK3399 上测试的。可以按照 [教程](https://gist.github.com/mli/585aed2cec0b5178b1a510f9f236afa2) 为 RK3399 设置 OS 及 OpenCL 驱动程序。 + +在 rk3399 板上构建 runtime 也需启用 OpenCL。在 TVM 根目录下执行: + +``` bash +cp cmake/config.cmake . +sed -i "s/USE_OPENCL OFF/USE_OPENCL ON/" config.cmake +make runtime -j4 +``` +::: + +下面的函数展示了如何远程运行 OpenCL 内核: + +``` python +def run_opencl(): + # 注意:这是 rk3399 板的设置。你需要根据你的环境进行修改 + opencl_device_host = "10.77.1.145" + opencl_device_port = 9090 + target = tvm.target.Target("opencl", host="llvm -mtriple=aarch64-linux-gnu") + + # 为上面的计算声明 "add one" 创建 schedule + s = te.create_schedule(B.op) + xo, xi = s[B].split(B.op.axis[0], factor=32) + s[B].bind(xo, te.thread_axis("blockIdx.x")) + s[B].bind(xi, te.thread_axis("threadIdx.x")) + func = tvm.build(s, [A, B], target=target) + + remote = rpc.connect(opencl_device_host, opencl_device_port) + + # 导出并上传 + path = temp.relpath("lib_cl.tar") + func.export_library(path) + remote.upload(path) + func = remote.load_module("lib_cl.tar") + + # 运行 + dev = remote.cl() + a = tvm.nd.array(np.random.uniform(size=1024).astype(A.dtype), dev) + b = tvm.nd.array(np.zeros(1024, dtype=A.dtype), dev) + func(a, b) + np.testing.assert_equal(b.numpy(), a.numpy() + 1) + print("OpenCL test passed!") +``` + +## 总结 + +本教程介绍了 TVM 中的交叉编译和 RPC 功能。 + +* 在远程设备上设置 RPC 服务器。 +* 设置目标设备配置,使得可在本地机器上交叉编译内核。 +* 通过 RPC API 远程上传和运行内核。 + +[下载 Python 源代码:cross_compilation_and_rpc.py](https://tvm.apache.org/docs/_downloads/766206ab8f1fd80ac34d9816cb991a0d/cross_compilation_and_rpc.py) + +[下载 Jupyter Notebook:cross_compilation_and_rpc.ipynb](https://tvm.apache.org/docs/_downloads/f289ca2466fcf79c024068c1f8642bd0/cross_compilation_and_rpc.ipynb) diff --git a/versioned_docs/version-0.12.0/tutorial/11-quick_start.md b/versioned_docs/version-0.12.0/tutorial/11-quick_start.md new file mode 100644 index 00000000..3a4c0e1f --- /dev/null +++ b/versioned_docs/version-0.12.0/tutorial/11-quick_start.md @@ -0,0 +1,253 @@ +--- +title: 快速入门:编译深度学习模型 +--- + +# 快速入门:编译深度学习模型 + +:::note +单击 [此处](https://tvm.apache.org/docs/tutorial/relay_quick_start.html#sphx-glr-download-tutorial-relay-quick-start-py) 下载完整的示例代码 +::: + +**作者**:[Yao Wang](https://github.com/kevinthesun),[Truman Tian](https://github.com/SiNZeRo) + +这个例子展示了如何用 Relay Python 前端构建神经网络,并为装有 TVM 的 NVIDIA GPU 生成 runtime 库。注意,构建 TVM 需要启用 CUDA 和 LLVM。 + +## TVM 支持的硬件后端概述 + +下图显示了 TVM 当前支持的硬件后端: + + ![https://github.com/dmlc/web-data/raw/main/tvm/tutorial/tvm_support_list.png](https://github.com/dmlc/web-data/raw/main/tvm/tutorial/tvm_support_list.png) + +本教程将选择 CUDA 和 LLVM 作为目标后端。首先,导入 Relay 和 TVM。 + +``` python +import numpy as np + +from tvm import relay +from tvm.relay import testing +import tvm +from tvm import te +from tvm.contrib import graph_executor +import tvm.testing +``` + +## 在 Relay 中定义神经网络 + +首先,定义一个带有 Relay Python 前端的神经网络。简单起见,我们在 Relay 中使用预定义的 resnet-18 网络。参数用 Xavier 初始化程序进行初始化。 Relay 还支持其他模型格式,如 MXNet、CoreML、ONNX 和 Tensorflow。 + +本教程假设在我们的设备上进行推理,并将 batch size 设置为 1。输入图像是大小为 224 * 224 的 RGB 彩色图像。调用 `tvm.relay.expr.TupleWrapper.astext()` 可以查看网络结构。 + +``` python +batch_size = 1 +num_class = 1000 +image_shape = (3, 224, 224) +data_shape = (batch_size,) + image_shape +out_shape = (batch_size, num_class) + +mod, params = relay.testing.resnet.get_workload( + num_layers=18, batch_size=batch_size, image_shape=image_shape +) + +# 想显示元数据则设置 show_meta_data=True +print(mod.astext(show_meta_data=False)) +``` + +访问代码中的 mod.astext 更多信息:[https://tvm.apache.org/docs/reference/api/python/ir.html#tvm.ir.Node.astext](https://tvm.apache.org/docs/reference/api/python/ir.html#tvm.ir.Node.astext) + +输出结果: + +``` bash +#[version = "0.0.5"] +def @main(%data: Tensor[(1, 3, 224, 224), float32] /* ty=Tensor[(1, 3, 224, 224), float32] */, %bn_data_gamma: Tensor[(3), float32] /* ty=Tensor[(3), float32] */, %bn_data_beta: Tensor[(3), float32] /* ty=Tensor[(3), float32] */, %bn_data_moving_mean: Tensor[(3), float32] /* ty=Tensor[(3), float32] */, %bn_data_moving_var: Tensor[(3), float32] /* ty=Tensor[(3), float32] */, %conv0_weight: Tensor[(64, 3, 7, 7), float32] /* ty=Tensor[(64, 3, 7, 7), float32] */, %bn0_gamma: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %bn0_beta: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %bn0_moving_mean: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %bn0_moving_var: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit1_bn1_gamma: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit1_bn1_beta: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit1_bn1_moving_mean: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit1_bn1_moving_var: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit1_conv1_weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */, %stage1_unit1_bn2_gamma: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit1_bn2_beta: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit1_bn2_moving_mean: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit1_bn2_moving_var: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit1_conv2_weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */, %stage1_unit1_sc_weight: Tensor[(64, 64, 1, 1), float32] /* ty=Tensor[(64, 64, 1, 1), float32] */, %stage1_unit2_bn1_gamma: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit2_bn1_beta: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit2_bn1_moving_mean: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit2_bn1_moving_var: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit2_conv1_weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */, %stage1_unit2_bn2_gamma: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit2_bn2_beta: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit2_bn2_moving_mean: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit2_bn2_moving_var: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage1_unit2_conv2_weight: Tensor[(64, 64, 3, 3), float32] /* ty=Tensor[(64, 64, 3, 3), float32] */, %stage2_unit1_bn1_gamma: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage2_unit1_bn1_beta: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage2_unit1_bn1_moving_mean: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage2_unit1_bn1_moving_var: Tensor[(64), float32] /* ty=Tensor[(64), float32] */, %stage2_unit1_conv1_weight: Tensor[(128, 64, 3, 3), float32] /* ty=Tensor[(128, 64, 3, 3), float32] */, %stage2_unit1_bn2_gamma: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit1_bn2_beta: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit1_bn2_moving_mean: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit1_bn2_moving_var: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit1_conv2_weight: Tensor[(128, 128, 3, 3), float32] /* ty=Tensor[(128, 128, 3, 3), float32] */, %stage2_unit1_sc_weight: Tensor[(128, 64, 1, 1), float32] /* ty=Tensor[(128, 64, 1, 1), float32] */, %stage2_unit2_bn1_gamma: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit2_bn1_beta: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit2_bn1_moving_mean: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit2_bn1_moving_var: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit2_conv1_weight: Tensor[(128, 128, 3, 3), float32] /* ty=Tensor[(128, 128, 3, 3), float32] */, %stage2_unit2_bn2_gamma: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit2_bn2_beta: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit2_bn2_moving_mean: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit2_bn2_moving_var: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage2_unit2_conv2_weight: Tensor[(128, 128, 3, 3), float32] /* ty=Tensor[(128, 128, 3, 3), float32] */, %stage3_unit1_bn1_gamma: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage3_unit1_bn1_beta: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage3_unit1_bn1_moving_mean: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage3_unit1_bn1_moving_var: Tensor[(128), float32] /* ty=Tensor[(128), float32] */, %stage3_unit1_conv1_weight: Tensor[(256, 128, 3, 3), float32] /* ty=Tensor[(256, 128, 3, 3), float32] */, %stage3_unit1_bn2_gamma: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit1_bn2_beta: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit1_bn2_moving_mean: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit1_bn2_moving_var: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit1_conv2_weight: Tensor[(256, 256, 3, 3), float32] /* ty=Tensor[(256, 256, 3, 3), float32] */, %stage3_unit1_sc_weight: Tensor[(256, 128, 1, 1), float32] /* ty=Tensor[(256, 128, 1, 1), float32] */, %stage3_unit2_bn1_gamma: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit2_bn1_beta: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit2_bn1_moving_mean: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit2_bn1_moving_var: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit2_conv1_weight: Tensor[(256, 256, 3, 3), float32] /* ty=Tensor[(256, 256, 3, 3), float32] */, %stage3_unit2_bn2_gamma: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit2_bn2_beta: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit2_bn2_moving_mean: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit2_bn2_moving_var: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage3_unit2_conv2_weight: Tensor[(256, 256, 3, 3), float32] /* ty=Tensor[(256, 256, 3, 3), float32] */, %stage4_unit1_bn1_gamma: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage4_unit1_bn1_beta: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage4_unit1_bn1_moving_mean: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage4_unit1_bn1_moving_var: Tensor[(256), float32] /* ty=Tensor[(256), float32] */, %stage4_unit1_conv1_weight: Tensor[(512, 256, 3, 3), float32] /* ty=Tensor[(512, 256, 3, 3), float32] */, %stage4_unit1_bn2_gamma: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit1_bn2_beta: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit1_bn2_moving_mean: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit1_bn2_moving_var: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit1_conv2_weight: Tensor[(512, 512, 3, 3), float32] /* ty=Tensor[(512, 512, 3, 3), float32] */, %stage4_unit1_sc_weight: Tensor[(512, 256, 1, 1), float32] /* ty=Tensor[(512, 256, 1, 1), float32] */, %stage4_unit2_bn1_gamma: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit2_bn1_beta: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit2_bn1_moving_mean: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit2_bn1_moving_var: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit2_conv1_weight: Tensor[(512, 512, 3, 3), float32] /* ty=Tensor[(512, 512, 3, 3), float32] */, %stage4_unit2_bn2_gamma: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit2_bn2_beta: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit2_bn2_moving_mean: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit2_bn2_moving_var: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %stage4_unit2_conv2_weight: Tensor[(512, 512, 3, 3), float32] /* ty=Tensor[(512, 512, 3, 3), float32] */, %bn1_gamma: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %bn1_beta: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %bn1_moving_mean: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %bn1_moving_var: Tensor[(512), float32] /* ty=Tensor[(512), float32] */, %fc1_weight: Tensor[(1000, 512), float32] /* ty=Tensor[(1000, 512), float32] */, %fc1_bias: Tensor[(1000), float32] /* ty=Tensor[(1000), float32] */) -> Tensor[(1, 1000), float32] { + %0 = nn.batch_norm(%data, %bn_data_gamma, %bn_data_beta, %bn_data_moving_mean, %bn_data_moving_var, epsilon=2e-05f, scale=False) /* ty=(Tensor[(1, 3, 224, 224), float32], Tensor[(3), float32], Tensor[(3), float32]) */; + %1 = %0.0 /* ty=Tensor[(1, 3, 224, 224), float32] */; + %2 = nn.conv2d(%1, %conv0_weight, strides=[2, 2], padding=[3, 3, 3, 3], channels=64, kernel_size=[7, 7]) /* ty=Tensor[(1, 64, 112, 112), float32] */; + %3 = nn.batch_norm(%2, %bn0_gamma, %bn0_beta, %bn0_moving_mean, %bn0_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 64, 112, 112), float32], Tensor[(64), float32], Tensor[(64), float32]) */; + %4 = %3.0 /* ty=Tensor[(1, 64, 112, 112), float32] */; + %5 = nn.relu(%4) /* ty=Tensor[(1, 64, 112, 112), float32] */; + %6 = nn.max_pool2d(%5, pool_size=[3, 3], strides=[2, 2], padding=[1, 1, 1, 1]) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %7 = nn.batch_norm(%6, %stage1_unit1_bn1_gamma, %stage1_unit1_bn1_beta, %stage1_unit1_bn1_moving_mean, %stage1_unit1_bn1_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 64, 56, 56), float32], Tensor[(64), float32], Tensor[(64), float32]) */; + %8 = %7.0 /* ty=Tensor[(1, 64, 56, 56), float32] */; + %9 = nn.relu(%8) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %10 = nn.conv2d(%9, %stage1_unit1_conv1_weight, padding=[1, 1, 1, 1], channels=64, kernel_size=[3, 3]) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %11 = nn.batch_norm(%10, %stage1_unit1_bn2_gamma, %stage1_unit1_bn2_beta, %stage1_unit1_bn2_moving_mean, %stage1_unit1_bn2_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 64, 56, 56), float32], Tensor[(64), float32], Tensor[(64), float32]) */; + %12 = %11.0 /* ty=Tensor[(1, 64, 56, 56), float32] */; + %13 = nn.relu(%12) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %14 = nn.conv2d(%13, %stage1_unit1_conv2_weight, padding=[1, 1, 1, 1], channels=64, kernel_size=[3, 3]) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %15 = nn.conv2d(%9, %stage1_unit1_sc_weight, padding=[0, 0, 0, 0], channels=64, kernel_size=[1, 1]) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %16 = add(%14, %15) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %17 = nn.batch_norm(%16, %stage1_unit2_bn1_gamma, %stage1_unit2_bn1_beta, %stage1_unit2_bn1_moving_mean, %stage1_unit2_bn1_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 64, 56, 56), float32], Tensor[(64), float32], Tensor[(64), float32]) */; + %18 = %17.0 /* ty=Tensor[(1, 64, 56, 56), float32] */; + %19 = nn.relu(%18) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %20 = nn.conv2d(%19, %stage1_unit2_conv1_weight, padding=[1, 1, 1, 1], channels=64, kernel_size=[3, 3]) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %21 = nn.batch_norm(%20, %stage1_unit2_bn2_gamma, %stage1_unit2_bn2_beta, %stage1_unit2_bn2_moving_mean, %stage1_unit2_bn2_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 64, 56, 56), float32], Tensor[(64), float32], Tensor[(64), float32]) */; + %22 = %21.0 /* ty=Tensor[(1, 64, 56, 56), float32] */; + %23 = nn.relu(%22) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %24 = nn.conv2d(%23, %stage1_unit2_conv2_weight, padding=[1, 1, 1, 1], channels=64, kernel_size=[3, 3]) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %25 = add(%24, %16) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %26 = nn.batch_norm(%25, %stage2_unit1_bn1_gamma, %stage2_unit1_bn1_beta, %stage2_unit1_bn1_moving_mean, %stage2_unit1_bn1_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 64, 56, 56), float32], Tensor[(64), float32], Tensor[(64), float32]) */; + %27 = %26.0 /* ty=Tensor[(1, 64, 56, 56), float32] */; + %28 = nn.relu(%27) /* ty=Tensor[(1, 64, 56, 56), float32] */; + %29 = nn.conv2d(%28, %stage2_unit1_conv1_weight, strides=[2, 2], padding=[1, 1, 1, 1], channels=128, kernel_size=[3, 3]) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %30 = nn.batch_norm(%29, %stage2_unit1_bn2_gamma, %stage2_unit1_bn2_beta, %stage2_unit1_bn2_moving_mean, %stage2_unit1_bn2_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 128, 28, 28), float32], Tensor[(128), float32], Tensor[(128), float32]) */; + %31 = %30.0 /* ty=Tensor[(1, 128, 28, 28), float32] */; + %32 = nn.relu(%31) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %33 = nn.conv2d(%32, %stage2_unit1_conv2_weight, padding=[1, 1, 1, 1], channels=128, kernel_size=[3, 3]) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %34 = nn.conv2d(%28, %stage2_unit1_sc_weight, strides=[2, 2], padding=[0, 0, 0, 0], channels=128, kernel_size=[1, 1]) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %35 = add(%33, %34) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %36 = nn.batch_norm(%35, %stage2_unit2_bn1_gamma, %stage2_unit2_bn1_beta, %stage2_unit2_bn1_moving_mean, %stage2_unit2_bn1_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 128, 28, 28), float32], Tensor[(128), float32], Tensor[(128), float32]) */; + %37 = %36.0 /* ty=Tensor[(1, 128, 28, 28), float32] */; + %38 = nn.relu(%37) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %39 = nn.conv2d(%38, %stage2_unit2_conv1_weight, padding=[1, 1, 1, 1], channels=128, kernel_size=[3, 3]) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %40 = nn.batch_norm(%39, %stage2_unit2_bn2_gamma, %stage2_unit2_bn2_beta, %stage2_unit2_bn2_moving_mean, %stage2_unit2_bn2_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 128, 28, 28), float32], Tensor[(128), float32], Tensor[(128), float32]) */; + %41 = %40.0 /* ty=Tensor[(1, 128, 28, 28), float32] */; + %42 = nn.relu(%41) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %43 = nn.conv2d(%42, %stage2_unit2_conv2_weight, padding=[1, 1, 1, 1], channels=128, kernel_size=[3, 3]) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %44 = add(%43, %35) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %45 = nn.batch_norm(%44, %stage3_unit1_bn1_gamma, %stage3_unit1_bn1_beta, %stage3_unit1_bn1_moving_mean, %stage3_unit1_bn1_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 128, 28, 28), float32], Tensor[(128), float32], Tensor[(128), float32]) */; + %46 = %45.0 /* ty=Tensor[(1, 128, 28, 28), float32] */; + %47 = nn.relu(%46) /* ty=Tensor[(1, 128, 28, 28), float32] */; + %48 = nn.conv2d(%47, %stage3_unit1_conv1_weight, strides=[2, 2], padding=[1, 1, 1, 1], channels=256, kernel_size=[3, 3]) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %49 = nn.batch_norm(%48, %stage3_unit1_bn2_gamma, %stage3_unit1_bn2_beta, %stage3_unit1_bn2_moving_mean, %stage3_unit1_bn2_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 256, 14, 14), float32], Tensor[(256), float32], Tensor[(256), float32]) */; + %50 = %49.0 /* ty=Tensor[(1, 256, 14, 14), float32] */; + %51 = nn.relu(%50) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %52 = nn.conv2d(%51, %stage3_unit1_conv2_weight, padding=[1, 1, 1, 1], channels=256, kernel_size=[3, 3]) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %53 = nn.conv2d(%47, %stage3_unit1_sc_weight, strides=[2, 2], padding=[0, 0, 0, 0], channels=256, kernel_size=[1, 1]) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %54 = add(%52, %53) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %55 = nn.batch_norm(%54, %stage3_unit2_bn1_gamma, %stage3_unit2_bn1_beta, %stage3_unit2_bn1_moving_mean, %stage3_unit2_bn1_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 256, 14, 14), float32], Tensor[(256), float32], Tensor[(256), float32]) */; + %56 = %55.0 /* ty=Tensor[(1, 256, 14, 14), float32] */; + %57 = nn.relu(%56) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %58 = nn.conv2d(%57, %stage3_unit2_conv1_weight, padding=[1, 1, 1, 1], channels=256, kernel_size=[3, 3]) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %59 = nn.batch_norm(%58, %stage3_unit2_bn2_gamma, %stage3_unit2_bn2_beta, %stage3_unit2_bn2_moving_mean, %stage3_unit2_bn2_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 256, 14, 14), float32], Tensor[(256), float32], Tensor[(256), float32]) */; + %60 = %59.0 /* ty=Tensor[(1, 256, 14, 14), float32] */; + %61 = nn.relu(%60) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %62 = nn.conv2d(%61, %stage3_unit2_conv2_weight, padding=[1, 1, 1, 1], channels=256, kernel_size=[3, 3]) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %63 = add(%62, %54) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %64 = nn.batch_norm(%63, %stage4_unit1_bn1_gamma, %stage4_unit1_bn1_beta, %stage4_unit1_bn1_moving_mean, %stage4_unit1_bn1_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 256, 14, 14), float32], Tensor[(256), float32], Tensor[(256), float32]) */; + %65 = %64.0 /* ty=Tensor[(1, 256, 14, 14), float32] */; + %66 = nn.relu(%65) /* ty=Tensor[(1, 256, 14, 14), float32] */; + %67 = nn.conv2d(%66, %stage4_unit1_conv1_weight, strides=[2, 2], padding=[1, 1, 1, 1], channels=512, kernel_size=[3, 3]) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %68 = nn.batch_norm(%67, %stage4_unit1_bn2_gamma, %stage4_unit1_bn2_beta, %stage4_unit1_bn2_moving_mean, %stage4_unit1_bn2_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 512, 7, 7), float32], Tensor[(512), float32], Tensor[(512), float32]) */; + %69 = %68.0 /* ty=Tensor[(1, 512, 7, 7), float32] */; + %70 = nn.relu(%69) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %71 = nn.conv2d(%70, %stage4_unit1_conv2_weight, padding=[1, 1, 1, 1], channels=512, kernel_size=[3, 3]) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %72 = nn.conv2d(%66, %stage4_unit1_sc_weight, strides=[2, 2], padding=[0, 0, 0, 0], channels=512, kernel_size=[1, 1]) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %73 = add(%71, %72) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %74 = nn.batch_norm(%73, %stage4_unit2_bn1_gamma, %stage4_unit2_bn1_beta, %stage4_unit2_bn1_moving_mean, %stage4_unit2_bn1_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 512, 7, 7), float32], Tensor[(512), float32], Tensor[(512), float32]) */; + %75 = %74.0 /* ty=Tensor[(1, 512, 7, 7), float32] */; + %76 = nn.relu(%75) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %77 = nn.conv2d(%76, %stage4_unit2_conv1_weight, padding=[1, 1, 1, 1], channels=512, kernel_size=[3, 3]) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %78 = nn.batch_norm(%77, %stage4_unit2_bn2_gamma, %stage4_unit2_bn2_beta, %stage4_unit2_bn2_moving_mean, %stage4_unit2_bn2_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 512, 7, 7), float32], Tensor[(512), float32], Tensor[(512), float32]) */; + %79 = %78.0 /* ty=Tensor[(1, 512, 7, 7), float32] */; + %80 = nn.relu(%79) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %81 = nn.conv2d(%80, %stage4_unit2_conv2_weight, padding=[1, 1, 1, 1], channels=512, kernel_size=[3, 3]) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %82 = add(%81, %73) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %83 = nn.batch_norm(%82, %bn1_gamma, %bn1_beta, %bn1_moving_mean, %bn1_moving_var, epsilon=2e-05f) /* ty=(Tensor[(1, 512, 7, 7), float32], Tensor[(512), float32], Tensor[(512), float32]) */; + %84 = %83.0 /* ty=Tensor[(1, 512, 7, 7), float32] */; + %85 = nn.relu(%84) /* ty=Tensor[(1, 512, 7, 7), float32] */; + %86 = nn.global_avg_pool2d(%85) /* ty=Tensor[(1, 512, 1, 1), float32] */; + %87 = nn.batch_flatten(%86) /* ty=Tensor[(1, 512), float32] */; + %88 = nn.dense(%87, %fc1_weight, units=1000) /* ty=Tensor[(1, 1000), float32] */; + %89 = nn.bias_add(%88, %fc1_bias, axis=-1) /* ty=Tensor[(1, 1000), float32] */; + nn.softmax(%89) /* ty=Tensor[(1, 1000), float32] */ +} +``` + +## 编译 + +下一步是用 Relay/TVM 管道编译模型。用户可以指定编译的优化级别(目前这个值为 0 到 3)。优化 pass 包括算子融合、预计算、布局变换等。 + +`relay.build()` 返回三个组件:JSON 格式的执行图、目标硬件上编译此执行图的函数组成的 TVM 模块库,以及模型的 blobs 参数。在编译过程中,Relay 进行图级优化,而 TVM 进行张量级优化,从而为模型服务提供优化的 runtime 模块。 + +首先为 NVIDIA GPU 编译。在这个过程中,`relay.build()` 首先进行了一些图级优化,例如剪枝、融合等,然后将算子(即优化图的节点)注册到 TVM 的实现,从而生成 *tvm.module*。为了生成模块库,TVM 首先将高级 IR 转换为指定目标后端的底层固有 IR,本例为 CUDA。然后生成的机器码将作为模块库。 + +``` python +opt_level = 3 +target = tvm.target.cuda() +with tvm.transform.PassContext(opt_level=opt_level): + lib = relay.build(mod, target, params=params) +``` + +输出结果: + +``` bash +/workspace/python/tvm/target/target.py:377: UserWarning: Try specifying cuda arch by adding 'arch=sm_xx' to your target. + warnings.warn("Try specifying cuda arch by adding 'arch=sm_xx' to your target.") +/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead. + "target_host parameter is going to be deprecated. " +``` + +## 运行生成库 + +创建图执行器,然后在 NVIDIA GPU 上运行该模块。 + +``` python +# create random input +dev = tvm.cuda() +data = np.random.uniform(-1, 1, size=data_shape).astype("float32") +# create module +module = graph_executor.GraphModule(lib["default"](dev)) +# set input and parameters +module.set_input("data", data) +# run +module.run() +# get output +out = module.get_output(0, tvm.nd.empty(out_shape)).numpy() + +# Print first 10 elements of output +print(out.flatten()[0:10]) +``` + +输出结果: + +``` bash +[0.00089283 0.00103331 0.0009094 0.00102275 0.00108751 0.00106737 + 0.00106262 0.00095838 0.00110792 0.00113151] +``` + +## 保存和加载编译模块 + +还可将计算图、库和参数保存到文件中,然后在部署环境中加载。 + +``` python +# 分别将计算图、库和参数保存到不同文件 +from tvm.contrib import utils + +temp = utils.tempdir() +path_lib = temp.relpath("deploy_lib.tar") +lib.export_library(path_lib) +print(temp.listdir()) +``` + +输出结果: + +``` bash +['deploy_lib.tar'] +``` + +``` python +# 重新加载模块 +loaded_lib = tvm.runtime.load_module(path_lib) +input_data = tvm.nd.array(data) + +module = graph_executor.GraphModule(loaded_lib["default"](dev)) +module.run(data=input_data) +out_deploy = module.get_output(0).numpy() + +# 打印输出的前十个元素 +print(out_deploy.flatten()[0:10]) + +# 检查来自部署模块的输出和原始输出是否一致 +tvm.testing.assert_allclose(out_deploy, out, atol=1e-5) +``` + +输出结果: + +``` bash +[0.00089283 0.00103331 0.0009094 0.00102275 0.00108751 0.00106737 + 0.00106262 0.00095838 0.00110792 0.00113151] +``` + +[下载 Python 源代码:relay_quick_start.py](https://tvm.apache.org/docs/_downloads/cc6d9aebd24d54d81752590cbc8f99f9/relay_quick_start.py) + +[下载 Jupyter Notebook:relay_quick_start.ipynb](https://tvm.apache.org/docs/_downloads/3dd2108354ac3028c96bcd6a0c7899dd/relay_quick_start.ipynb) diff --git a/versioned_docs/version-0.12.0/tutorial/12-uma.md b/versioned_docs/version-0.12.0/tutorial/12-uma.md new file mode 100644 index 00000000..510b2e1a --- /dev/null +++ b/versioned_docs/version-0.12.0/tutorial/12-uma.md @@ -0,0 +1,195 @@ +--- +title: 利用 UMA 使硬件加速器可直接用于 TVM +--- + +# 利用 UMA 使硬件加速器可直接用于 TVM + +:::note +单击 [此处](https://tvm.apache.org/docs/tutorial/uma.html#sphx-glr-download-tutorial-uma-py) 下载完整的示例代码 +::: + +**作者**:[Michael J. Klaiber](https://github.com/MichaelJKlaiber),[Christoph Gerum](https://github.com/cgerum),[Paul Palomero Bernardo](https://github.com/PaulPalomeroBernardo/) + +本节介绍**通用模块化加速器接口**(UMA)。UMA 提供了一个易用的 API 来将新的硬件加速器集成到 TVM 中。 + +本教程详细介绍了如何利用 UMA 使得你的硬件加速器可直接用于 TVM。虽然这个问题没有万能的解决方案,但 UMA 旨在提供一个稳定的纯 Python API,从而将许多种类的硬件加速器集成到 TVM 中。 + +本教程将通过三个逐渐复杂的用例来介绍 UMA API。这些用例引入了三个模拟加速器 **Vanilla**、**Strawberry** 和 **Chocolate**,并用 UMA 将它们集成到 TVM 中。 + +## Vanilla + +**Vanilla** 是一个由 MAC 数组组成的简单加速器,没有内部存储器。它只能处理 Conv2D 层,所有其他层都在 CPU 上执行,同时也协调 **Vanilla**。 CPU 和 Vanilla 共享内存。 + +**Vanilla** 的 C 接口 `vanilla_conv2dnchw(...)` 用于执行 Conv2D 操作(包括 same-padding),它接收指向输入特征图、权重和结果的指针,以及 Conv2D 的维度:*oc*、*iw*、*ih*、*ic*、*kh* 和 *kw*。 + +``` cpp +int vanilla_conv2dnchw(float* ifmap, float* weights, float* result, int oc, int iw, int ih, int ic, int kh, int kw); +``` + +脚本 *uma_cli* 为新的加速器创建带有 API(UMA-API)调用的代码骨架。 + +**Vanilla** 的使用方式如下:(`--tutorial vanilla` 添加了本部分教程所需的所有附加文件) + +``` bash +pip install inflection +cd $TVM_HOME/apps/uma +python uma_cli.py --add_hardware vanilla_accelerator --tutorial vanilla +``` + +uma_cli.py 在 `vanilla_accelerator` 目录中生成这些文件。 + +``` bash +backend.py +codegen.py +conv2dnchw.cc +passes.py +patterns.py +run.py +strategies.py +``` + +Vanilla 后端 + +vanilla 生成的后端位于 *vanilla_accelerator/backend.py* 中: + +``` python +class VanillaAcceleratorBackend(UMABackend): + """VanillaAccelerator 的 UMA 后端。""" + + def __init__(self): + super().__init__() + + self._register_pattern("conv2d", conv2d_pattern()) + self._register_tir_pass(PassPhase.TIR_PHASE_0, VanillaAcceleratorConv2DPass()) + self._register_codegen(fmt="c", includes=gen_includes) + + @property + def target_name(self): + return "vanilla_accelerator" +``` + +定义迁移模式 + +为了指定 *Conv2D* 迁移到 **Vanilla**,*vanilla_accelerator/patterns.py* 中将其描述为 Relay 数据流模式([DFPattern](https://tvm.apache.org/docs/reference/langref/relay_pattern.html))。 + +``` python +def conv2d_pattern(): + pattern = is_op("nn.conv2d")(wildcard(), wildcard()) + pattern = pattern.has_attr({"strides": [1, 1]}) + return pattern +``` + +为了将输入计算图的 **Conv2D** 算子映射到 **Vanilla** 的底层函数调用 `vanilla_conv2dnchw(...)`,在 VanillaAcceleratorBackend 中注册了 TIR pass *VanillaAcceleratorConv2DPass*(稍后讨论)。 + +Codegen + +文件 `vanilla_accelerator/codegen.py` 定义了静态 C 代码,它被添加到生成的结果 C 代码(由 `gen_includes` 中的 TVM 的 C-Codegen 生成)中,其目的是包含 **Vanilla** 的底层库 `vanilla_conv2dnchw()`。 + +``` python +def gen_includes() -> str: + topdir = pathlib.Path(__file__).parent.absolute() + + includes = "" + includes += f'#include "{topdir}/conv2dnchw.cc"' + return includes +``` + +如上面的 *VanillaAcceleratorBackend* 所示,用 *self._register_codegen* 可将其注册到 UMA。 + +``` python +self._register_codegen(fmt="c", includes=gen_includes) +``` + +构建神经网络并在 Vanilla 上运行 + +为了演示 UMA 的功能,将为单个 Conv2D 层生成 C 代码,并在 Vanilla 加速器上运行。文件 `vanilla_accelerator/run.py` 提供了一个使用 Vanilla 的 C-API 运行 Conv2D 层的 demo。 + +``` python +def main(): + mod, inputs, output_list, runner = create_conv2d() + + uma_backend = VanillaAcceleratorBackend() + uma_backend.register() + mod = uma_backend.partition(mod) + target = tvm.target.Target("vanilla_accelerator", host=tvm.target.Target("c")) + + export_directory = tvm.contrib.utils.tempdir(keep_for_debug=True).path + print(f"Generated files are in {export_directory}") + compile_and_run( + AOTModel(module=mod, inputs=inputs, outputs=output_list), + runner, + interface_api="c", + use_unpacked_api=True, + target=target, + test_dir=str(export_directory), + ) + +main() +``` + +运行 `vanilla_accelerator/run.py`,将以模型库格式(MLF)生成输出文件。 + +输出结果: + +``` bash +Generated files are in /tmp/tvm-debug-mode-tempdirs/2022-07-13T13-26-22___x5u76h0p/00000 +``` + +查看生成的文件: + +输出结果: + +``` bash +cd /tmp/tvm-debug-mode-tempdirs/2022-07-13T13-26-22___x5u76h0p/00000 +cd build/ +ls -1 + +codegen +lib.tar +metadata.json +parameters +runtime +src +``` + +若要评估生成的 C 代码,请查看 `codegen/host/src/default_lib2.c`。 + +``` bash +cd codegen/host/src/ +ls -1 + +default_lib0.c +default_lib1.c +default_lib2.c +``` + +在 *default_lib2.c* 中,可以看到生成的代码调用了 Vanilla 的 C-API,然后执行了一个 Conv2D 层: + +``` cpp +TVM_DLL int32_t tvmgen_default_vanilla_accelerator_main_0(float* placeholder, float* placeholder1, float* conv2d_nchw, uint8_t* global_workspace_1_var) { + vanilla_accelerator_conv2dnchw(placeholder, placeholder1, conv2d_nchw, 32, 14, 14, 32, 3, 3); + return 0; +} +``` + +## Strawberry + +即将上线 + +## Chocolate + +即将上线 + +## 征求社区意见 + +若本教程**不**适合你的加速器,请将你的需求添加到 TVM 论坛中的 [UMA 帖子](https://discuss.tvm.apache.org/t/rfc-uma-universal-modular-accelerator-interface/12039) 中。我们很乐意通过扩展本教程来提供更多指导,例如如何利用 UMA 接口使得更多种类的 AI 硬件加速器可直接用于 TVM。 + +## 参考 + +[UMA-RFC] [UMA:通用模块化加速器接口](https://github.com/apache/tvm-rfcs/blob/main/rfcs/0060_UMA_Unified_Modular_Accelerator_Interface.md),TVM RFC,2022 年 6 月。 + +[DFPattern] [Relay 中的模式匹配](https://tvm.apache.org/docs/reference/langref/relay_pattern.html) + +[下载 Python 源代码:uma.py](https://tvm.apache.org/docs/_downloads/f9c6910c7b4a120c51a9bf48f34f3ad7/uma.py) + +[下载 Jupyter Notebook:uma.ipynb](https://tvm.apache.org/docs/_downloads/6e0673ce1f08636c34d0b9a73ea114f7/uma.ipynb) diff --git a/versioned_docs/version-0.12.0/tutorial/13-TOPI.md b/versioned_docs/version-0.12.0/tutorial/13-TOPI.md new file mode 100644 index 00000000..f4a5d609 --- /dev/null +++ b/versioned_docs/version-0.12.0/tutorial/13-TOPI.md @@ -0,0 +1,460 @@ +--- +title: TOPI 简介 +--- + +# TOPI 简介 + +:::note +单击 [此处](https://tvm.apache.org/docs/tutorial/intro_topi.html#sphx-glr-download-tutorial-intro-topi-py) 下载完整的示例代码 +::: + +**作者**:[Ehsan M. Kermani](https://github.com/ehsanmok) + +这是 TVM 算子清单(TOPI)的入门教程。 TOPI 提供了 numpy 风格的通用操作和 schedule,其抽象程度高于 TVM。本教程将介绍 TOPI 是如何使得 TVM 中的代码不那么样板化的。 + +``` python +import tvm +import tvm.testing +from tvm import te +from tvm import topi +import numpy as np +``` + +## 基本示例 + +让我们回顾一下行求和操作(例如 `B = numpy.sum(A, axis=1)`)。要计算二维 TVM 张量 A 的行之和,应指定符号运算以及 schedule,如下所示: + +``` python +n = te.var("n") +m = te.var("m") +A = te.placeholder((n, m), name="A") +k = te.reduce_axis((0, m), "k") +B = te.compute((n,), lambda i: te.sum(A[i, k], axis=k), name="B") +s = te.create_schedule(B.op) +``` + +输入以下命令查看可读的 IR 代码: + +``` bash +print(tvm.lower(s, [A], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [(stride: int32*n: int32)], [], type="auto")} + buffer_map = {A_1: A} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [n, m: int32], [stride, stride_1: int32], type="auto")} { + allocate(B: Pointer(global float32), float32, [n]), storage_scope = global; + for (i: int32, 0, n) { + B_1: Buffer(B, float32, [n], [])[i] = 0f32 + for (k: int32, 0, m) { + B_1[i] = (B_1[i] + A[((i*stride) + (k*stride_1))]) + } + } +} +``` + +然而,必须为这样一个常用的操作定义 reduce 轴,并用 `te.compute` 定义显式计算。幸运的是,可以用 `topi.sum`(类似 `numpy.sum`)来替换这两行: + +``` python +C = topi.sum(A, axis=1) +ts = te.create_schedule(C.op) +print(tvm.lower(ts, [A], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(A_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {A: Buffer(A_2: Pointer(float32), float32, [(stride: int32*n: int32)], [], type="auto")} + buffer_map = {A_1: A} + preflattened_buffer_map = {A_1: A_3: Buffer(A_2, float32, [n, m: int32], [stride, stride_1: int32], type="auto")} { + allocate(A_red: Pointer(global float32), float32, [n]), storage_scope = global; + for (ax0: int32, 0, n) { + A_red_1: Buffer(A_red, float32, [n], [])[ax0] = 0f32 + for (k1: int32, 0, m) { + A_red_1[ax0] = (A_red_1[ax0] + A[((ax0*stride) + (k1*stride_1))]) + } + } +} +``` + +## Numpy 风格的算子重载 + +可用 `topi.broadcast_add` 添加两个张量(其 shape 可广播,且是特定的)。TOPI 为此类常见操作提供了算子重载使其更简短。例如: + +``` python +x, y = 100, 10 +a = te.placeholder((x, y, y), name="a") +b = te.placeholder((y, y), name="b") +c = a + b # 等价于 topi.broadcast_add +d = a * b # 等价于 topi.broadcast_mul +``` + +TOPI 使用相同的语法重载,将原语 (*int, float*) 广播到张量 `d - 3.14`。 + +## 通用调度和融合操作 + +前面已经展示了 TOPI 如何使我们免于用低级 API 编写显式的计算过程,但调度过程还是和以前一样。TOPI 还基于给定的上下文提供了更高级的调度方案。可以仅用 `topi.generic.schedule_reduce` 调度下面以 `topi.sum` 结尾的一系列操作,以 CUDA 为例: + +``` python +e = topi.elemwise_sum([c, d]) +f = e / 2.0 +g = topi.sum(f) +with tvm.target.cuda(): + sg = topi.cuda.schedule_reduce(g) + print(tvm.lower(sg, [a, b], simple_mode=True)) +``` + +输出结果: + +``` bash +/workspace/python/tvm/target/target.py:377: UserWarning: Try specifying cuda arch by adding 'arch=sm_xx' to your target. + warnings.warn("Try specifying cuda arch by adding 'arch=sm_xx' to your target.") +@main = primfn(a_1: handle, b_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {a: Buffer(a_2: Pointer(float32), float32, [10000], []), + b: Buffer(b_2: Pointer(float32), float32, [100], [])} + buffer_map = {a_1: a, b_1: b} + preflattened_buffer_map = {a_1: a_3: Buffer(a_2, float32, [100, 10, 10], []), b_1: b_3: Buffer(b_2, float32, [10, 10], [])} { + allocate(T_divide_red: Pointer(global float32), float32, [1]), storage_scope = global; + attr [IterVar(threadIdx.x: int32, [0:1024], "ThreadIndex", "threadIdx.x")] "thread_extent" = 1024; + allocate(T_divide_red.rf: Pointer(local float32), float32, [1]), storage_scope = local; + allocate(reduce_temp0: Pointer(local float32), float32, [1]), storage_scope = local { + T_divide_red.rf_1: Buffer(T_divide_red.rf, float32, [1], [], scope="local", align=4)[0] = 0f32 + for (k0.k1.fused.k2.fused.outer: int32, 0, 10) { + if @tir.likely((((((k0.k1.fused.k2.fused.outer*64) + floordiv(threadIdx.x, 16)) < 625) && (((k0.k1.fused.k2.fused.outer*64) + floordiv(threadIdx.x, 16)) < 625)) && (((k0.k1.fused.k2.fused.outer*64) + floordiv(threadIdx.x, 16)) < 625)), dtype=bool) { + T_divide_red.rf_1[0] = (T_divide_red.rf_1[0] + (((a[((k0.k1.fused.k2.fused.outer*1024) + threadIdx.x)] + b[((floordiv(floormod(((k0.k1.fused.k2.fused.outer*12) + floordiv(threadIdx.x, 2)), 50), 5)*10) + floormod(((k0.k1.fused.k2.fused.outer*4) + threadIdx.x), 10))]) + (a[((k0.k1.fused.k2.fused.outer*1024) + threadIdx.x)]*b[((floordiv(floormod(((k0.k1.fused.k2.fused.outer*12) + floordiv(threadIdx.x, 2)), 50), 5)*10) + floormod(((k0.k1.fused.k2.fused.outer*4) + threadIdx.x), 10))]))*0.5f32)) + } + } + attr [meta[tir.CommReducer][0]] "reduce_scope" = @tir.reinterpret(0u64, dtype=handle); + @tir.tvm_thread_allreduce(1u32, T_divide_red.rf_1[0], True, reduce_temp0_1: Buffer(reduce_temp0, float32, [1], [], scope="local")[0], threadIdx.x, dtype=handle) + if (threadIdx.x == 0) { + T_divide_red_1: Buffer(T_divide_red, float32, [1], [], align=4)[0] = reduce_temp0_1[0] + } + } +} +``` + +如上所示,计算的调度阶段是累积的,可以输入以下命令来查看: + +``` python +print(sg.stages) +``` + +输出结果: + +``` bash +[stage(a, placeholder(a, 0x228afb00)), stage(b, placeholder(b, 0x22097c90)), stage(T_add, compute(T_add, body=[(a[ax0, ax1, ax2] + b[ax1, ax2])], axis=[iter_var(ax0, range(min=0, ext=100)), iter_var(ax1, range(min=0, ext=10)), iter_var(ax2, range(min=0, ext=10))], reduce_axis=[], tag=broadcast, attrs={})), stage(T_multiply, compute(T_multiply, body=[(a[ax0, ax1, ax2]*b[ax1, ax2])], axis=[iter_var(ax0, range(min=0, ext=100)), iter_var(ax1, range(min=0, ext=10)), iter_var(ax2, range(min=0, ext=10))], reduce_axis=[], tag=broadcast, attrs={})), stage(T_elemwise_sum, compute(T_elemwise_sum, body=[(T_add[ax0, ax1, ax2] + T_multiply[ax0, ax1, ax2])], axis=[iter_var(ax0, range(min=0, ext=100)), iter_var(ax1, range(min=0, ext=10)), iter_var(ax2, range(min=0, ext=10))], reduce_axis=[], tag=elemwise, attrs={})), stage(T_divide, compute(T_divide, body=[(T_elemwise_sum[ax0, ax1, ax2]/2f)], axis=[iter_var(ax0, range(min=0, ext=100)), iter_var(ax1, range(min=0, ext=10)), iter_var(ax2, range(min=0, ext=10))], reduce_axis=[], tag=elemwise, attrs={})), stage(T_divide_red.rf, compute(T_divide_red.rf, body=[reduce(combiner=comm_reducer(result=[(x + y)], lhs=[x], rhs=[y], identity_element=[0f]), source=[T_divide[floordiv(floordiv((k0.k1.fused.k2.fused.inner + (k0.k1.fused.k2.fused.outer*1024)), 10), 10), floormod(floordiv((k0.k1.fused.k2.fused.inner + (k0.k1.fused.k2.fused.outer*1024)), 10), 10), floormod((k0.k1.fused.k2.fused.inner + (k0.k1.fused.k2.fused.outer*1024)), 10)]], init=[], axis=[iter_var(k0.k1.fused.k2.fused.outer, range(min=0, ext=10))], where=tir.likely((((floordiv(floordiv((k0.k1.fused.k2.fused.inner + (k0.k1.fused.k2.fused.outer*1024)), 10), 10) < 100) && (floordiv((k0.k1.fused.k2.fused.inner + (k0.k1.fused.k2.fused.outer*1024)), 10) < 1000)) && ((k0.k1.fused.k2.fused.inner + (k0.k1.fused.k2.fused.outer*1024)) < 10000))), value_index=0)], axis=[iter_var(k0.k1.fused.k2.fused.inner, range(min=0, ext=1024))], reduce_axis=[iter_var(k0.k1.fused.k2.fused.outer, range(min=0, ext=10))], tag=, attrs={})), stage(T_divide_red, compute(T_divide_red.repl, body=[reduce(combiner=comm_reducer(result=[(x + y)], lhs=[x], rhs=[y], identity_element=[0f]), source=[T_divide_red.rf[k0.k1.fused.k2.fused.inner.v]], init=[], axis=[iter_var(k0.k1.fused.k2.fused.inner.v, range(min=0, ext=1024))], where=(bool)1, value_index=0)], axis=[], reduce_axis=[iter_var(k0.k1.fused.k2.fused.inner.v, range(min=0, ext=1024))], tag=, attrs={}))] +``` + +可通过与 `numpy` 结果对比来验证其正确性,如下所示: + +``` python +func = tvm.build(sg, [a, b, g], "cuda") +dev = tvm.cuda(0) +a_np = np.random.uniform(size=(x, y, y)).astype(a.dtype) +b_np = np.random.uniform(size=(y, y)).astype(b.dtype) +g_np = np.sum(np.add(a_np + b_np, a_np * b_np) / 2.0) +a_nd = tvm.nd.array(a_np, dev) +b_nd = tvm.nd.array(b_np, dev) +g_nd = tvm.nd.array(np.zeros(g_np.shape, dtype=g_np.dtype), dev) +func(a_nd, b_nd, g_nd) +tvm.testing.assert_allclose(g_nd.numpy(), g_np, rtol=1e-5) +``` + +TOPI 还提供了常见神经网络操作,例如对优化的 schedule 进行 *softmax*: + +``` python +tarray = te.placeholder((512, 512), name="tarray") +softmax_topi = topi.nn.softmax(tarray) +with tvm.target.Target("cuda"): + sst = topi.cuda.schedule_softmax(softmax_topi) + print(tvm.lower(sst, [tarray], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(tarray_1: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {tarray: Buffer(tarray_2: Pointer(float32), float32, [262144], [])} + buffer_map = {tarray_1: tarray} + preflattened_buffer_map = {tarray_1: tarray_3: Buffer(tarray_2, float32, [512, 512], [])} { + allocate(T_softmax_norm: Pointer(global float32x4), float32x4, [65536]), storage_scope = global; + attr [IterVar(blockIdx.x: int32, (nullptr), "ThreadIndex", "blockIdx.x")] "thread_extent" = 512; + allocate(normal_reduce_temp0: Pointer(local float32), float32, [1]), storage_scope = local; + allocate(reduce_temp0: Pointer(local float32), float32, [1]), storage_scope = local; + allocate(T_softmax_exp: Pointer(warp float32), float32, [512]), storage_scope = warp; + allocate(normal_reduce_temp0_1: Pointer(local float32), float32, [1]), storage_scope = local; + allocate(reduce_temp0_1: Pointer(local float32), float32, [1]), storage_scope = local { + attr [IterVar(threadIdx.x: int32, [0:32], "ThreadIndex", "threadIdx.x")] "thread_extent" = 32 { + normal_reduce_temp0_2: Buffer(normal_reduce_temp0, float32, [1], [], scope="local")[0] = -3.40282e+38f32 + for (k.inner: int32, 0, 16) { + normal_reduce_temp0_2[0] = max(normal_reduce_temp0_2[0], tarray[(((blockIdx.x*512) + (threadIdx.x*16)) + k.inner)]) + } + attr [meta[tir.CommReducer][0]] "reduce_scope" = @tir.reinterpret(0u64, dtype=handle); + @tir.tvm_thread_allreduce(1u32, normal_reduce_temp0_2[0], True, reduce_temp0_2: Buffer(reduce_temp0, float32, [1], [], scope="local")[0], threadIdx.x, dtype=handle) + for (i1.inner.outer: int32, 0, 4) { + let cse_var_1: int32 = (i1.inner.outer*4) + T_softmax_exp_1: Buffer(T_softmax_exp, float32, [512], [], scope="warp")[ramp(((threadIdx.x*16) + cse_var_1), 1, 4)] = @tir.exp((tarray[ramp((((blockIdx.x*512) + (threadIdx.x*16)) + cse_var_1), 1, 4)] - broadcast(reduce_temp0_3: Buffer(reduce_temp0, float32, [1], [], scope="local", align=4)[0], 4)), dtype=float32x4) + } + } + attr [IterVar(threadIdx.x, [0:32], "ThreadIndex", "threadIdx.x")] "thread_extent" = 32 { + normal_reduce_temp0_3: Buffer(normal_reduce_temp0_1, float32, [1], [], scope="local")[0] = 0f32 + for (k.inner_1: int32, 0, 16) { + normal_reduce_temp0_3[0] = (normal_reduce_temp0_3[0] + T_softmax_exp_1[((threadIdx.x*16) + k.inner_1)]) + } + attr [meta[tir.CommReducer][1]] "reduce_scope" = @tir.reinterpret(0u64, dtype=handle); + @tir.tvm_thread_allreduce(1u32, normal_reduce_temp0_3[0], True, reduce_temp0_4: Buffer(reduce_temp0_1, float32, [1], [], scope="local")[0], threadIdx.x, dtype=handle) + for (i1.inner.outer_1: int32, 0, 4) { + T_softmax_norm_1: Buffer(T_softmax_norm, float32x4, [65536], [])[(((blockIdx.x*128) + (threadIdx.x*4)) + i1.inner.outer_1)] = (T_softmax_exp_1[ramp(((threadIdx.x*16) + (i1.inner.outer_1*4)), 1, 4)] / broadcast(reduce_temp0_5: Buffer(reduce_temp0_1, float32, [1], [], scope="local", align=4)[0], 4)) + } + } + } +} +``` + +## 融合卷积 + +可将 `topi.nn.conv2d` 和 `topi.nn.relu` 融合在一起。 + +:::note +TOPI 函数都是通用函数,不同的后端实现性能优化的方式不同。所有的后端都必须在 compute 声明和 schedule 范围内调用它们。 TVM 会选择调用目标信息的正确函数。 +::: + +``` python +data = te.placeholder((1, 3, 224, 224)) +kernel = te.placeholder((10, 3, 5, 5)) + +with tvm.target.Target("cuda"): + conv = topi.cuda.conv2d_nchw(data, kernel, 1, 2, 1) + out = topi.nn.relu(conv) + sconv = topi.cuda.schedule_conv2d_nchw([out]) + print(tvm.lower(sconv, [data, kernel], simple_mode=True)) +``` + +输出结果: + +``` bash +@main = primfn(placeholder_2: handle, placeholder_3: handle) -> () + attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True} + buffers = {placeholder: Buffer(placeholder_4: Pointer(float32), float32, [150528], []), + placeholder_1: Buffer(placeholder_5: Pointer(float32), float32, [750], [])} + buffer_map = {placeholder_2: placeholder, placeholder_3: placeholder_1} + preflattened_buffer_map = {placeholder_2: placeholder_6: Buffer(placeholder_4, float32, [1, 3, 224, 224], []), placeholder_3: placeholder_7: Buffer(placeholder_5, float32, [10, 3, 5, 5], [])} { + allocate(compute: Pointer(global float32), float32, [501760]), storage_scope = global; + attr [IterVar(blockIdx.z: int32, (nullptr), "ThreadIndex", "blockIdx.z")] "thread_extent" = 5; + allocate(conv2d_nchw: Pointer(local float32), float32, [14]), storage_scope = local; + allocate(pad_temp.shared: Pointer(shared float32), float32, [112]), storage_scope = shared; + allocate(placeholder.shared: Pointer(shared float32), float32, [2]), storage_scope = shared; + attr [IterVar(blockIdx.y: int32, (nullptr), "ThreadIndex", "blockIdx.y")] "thread_extent" = 224; + attr [IterVar(blockIdx.x: int32, (nullptr), "ThreadIndex", "blockIdx.x")] "thread_extent" = 2; + attr [IterVar(threadIdx.z: int32, (nullptr), "ThreadIndex", "threadIdx.z")] "thread_extent" = 1; + attr [IterVar(threadIdx.y: int32, (nullptr), "ThreadIndex", "threadIdx.y")] "thread_extent" = 1; + attr [IterVar(threadIdx.x: int32, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 16 { + conv2d_nchw_1: Buffer(conv2d_nchw, float32, [4], [], scope="local", align=8)[0] = 0f32 + conv2d_nchw_1[2] = 0f32 + conv2d_nchw_1[4] = 0f32 + conv2d_nchw_1[6] = 0f32 + conv2d_nchw_1[8] = 0f32 + conv2d_nchw_1[10] = 0f32 + conv2d_nchw_1[12] = 0f32 + conv2d_nchw_1[1] = 0f32 + conv2d_nchw_1[3] = 0f32 + conv2d_nchw_1[5] = 0f32 + conv2d_nchw_1[7] = 0f32 + conv2d_nchw_1[9] = 0f32 + conv2d_nchw_1[11] = 0f32 + conv2d_nchw_1[13] = 0f32 + for (rc.outer: int32, 0, 3) { + for (ry.outer: int32, 0, 5) { + attr [IterVar(threadIdx.z_1: int32, (nullptr), "ThreadIndex", "threadIdx.z")] "thread_extent" = 1; + attr [IterVar(threadIdx.y_1: int32, (nullptr), "ThreadIndex", "threadIdx.y")] "thread_extent" = 1; + attr [IterVar(threadIdx.x_1: int32, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 16 { + pad_temp.shared_1: Buffer(pad_temp.shared, float32, [112], [], scope="shared")[(threadIdx.x_1*7)] = @tir.if_then_else((((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)) && (1 <= ((blockIdx.x*56) + floordiv((threadIdx.x_1*7), 2)))), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 450)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 1)] = @tir.if_then_else((((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)) && (1 <= ((blockIdx.x*56) + floordiv(((threadIdx.x_1*7) + 1), 2)))), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 449)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 2)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 448)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 3)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 447)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 4)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 446)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 5)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 445)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 6)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 444)], 0f32, dtype=float32) + } + attr [IterVar(threadIdx.z_2: int32, (nullptr), "ThreadIndex", "threadIdx.z")] "thread_extent" = 1; + attr [IterVar(threadIdx.y_2: int32, (nullptr), "ThreadIndex", "threadIdx.y")] "thread_extent" = 1; + attr [IterVar(threadIdx.x_2: int32, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 16; + if @tir.likely((threadIdx.x_2 < 2), dtype=bool) { + placeholder.shared_1: Buffer(placeholder.shared, float32, [2], [], scope="shared", align=8)[threadIdx.x_2] = placeholder_1[((((blockIdx.z*150) + (threadIdx.x_2*75)) + (rc.outer*25)) + (ry.outer*5))] + } + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[threadIdx.x]*placeholder.shared_1[0])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[(threadIdx.x + 16)]*placeholder.shared_1[0])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[(threadIdx.x + 32)]*placeholder.shared_1[0])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[(threadIdx.x + 48)]*placeholder.shared_1[0])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[(threadIdx.x + 64)]*placeholder.shared_1[0])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[(threadIdx.x + 80)]*placeholder.shared_1[0])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[(threadIdx.x + 96)]*placeholder.shared_1[0])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[threadIdx.x]*placeholder.shared_1[1])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[(threadIdx.x + 16)]*placeholder.shared_1[1])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[(threadIdx.x + 32)]*placeholder.shared_1[1])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[(threadIdx.x + 48)]*placeholder.shared_1[1])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[(threadIdx.x + 64)]*placeholder.shared_1[1])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[(threadIdx.x + 80)]*placeholder.shared_1[1])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[(threadIdx.x + 96)]*placeholder.shared_1[1])) + attr [IterVar(threadIdx.z_1, (nullptr), "ThreadIndex", "threadIdx.z")] "thread_extent" = 1; + attr [IterVar(threadIdx.y_1, (nullptr), "ThreadIndex", "threadIdx.y")] "thread_extent" = 1; + attr [IterVar(threadIdx.x_1, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 16 { + pad_temp.shared_1[(threadIdx.x_1*7)] = @tir.if_then_else((((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)) && (1 <= ((blockIdx.x*56) + floordiv(((threadIdx.x_1*7) + 1), 2)))), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 449)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 1)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 448)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 2)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 447)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 3)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 446)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 4)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 445)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 5)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 444)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 6)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 443)], 0f32, dtype=float32) + } + attr [IterVar(threadIdx.z_2, (nullptr), "ThreadIndex", "threadIdx.z")] "thread_extent" = 1; + attr [IterVar(threadIdx.y_2, (nullptr), "ThreadIndex", "threadIdx.y")] "thread_extent" = 1; + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 16; + if @tir.likely((threadIdx.x_2 < 2), dtype=bool) { + placeholder.shared_1[threadIdx.x_2] = placeholder_1[(((((blockIdx.z*150) + (threadIdx.x_2*75)) + (rc.outer*25)) + (ry.outer*5)) + 1)] + } + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[threadIdx.x]*placeholder.shared_1[0])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[(threadIdx.x + 16)]*placeholder.shared_1[0])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[(threadIdx.x + 32)]*placeholder.shared_1[0])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[(threadIdx.x + 48)]*placeholder.shared_1[0])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[(threadIdx.x + 64)]*placeholder.shared_1[0])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[(threadIdx.x + 80)]*placeholder.shared_1[0])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[(threadIdx.x + 96)]*placeholder.shared_1[0])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[threadIdx.x]*placeholder.shared_1[1])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[(threadIdx.x + 16)]*placeholder.shared_1[1])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[(threadIdx.x + 32)]*placeholder.shared_1[1])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[(threadIdx.x + 48)]*placeholder.shared_1[1])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[(threadIdx.x + 64)]*placeholder.shared_1[1])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[(threadIdx.x + 80)]*placeholder.shared_1[1])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[(threadIdx.x + 96)]*placeholder.shared_1[1])) + attr [IterVar(threadIdx.z_1, (nullptr), "ThreadIndex", "threadIdx.z")] "thread_extent" = 1; + attr [IterVar(threadIdx.y_1, (nullptr), "ThreadIndex", "threadIdx.y")] "thread_extent" = 1; + attr [IterVar(threadIdx.x_1, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 16 { + pad_temp.shared_1[(threadIdx.x_1*7)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 448)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 1)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 447)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 2)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 446)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 3)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 445)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 4)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 444)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 5)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 443)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 6)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 442)], 0f32, dtype=float32) + } + attr [IterVar(threadIdx.z_2, (nullptr), "ThreadIndex", "threadIdx.z")] "thread_extent" = 1; + attr [IterVar(threadIdx.y_2, (nullptr), "ThreadIndex", "threadIdx.y")] "thread_extent" = 1; + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 16; + if @tir.likely((threadIdx.x_2 < 2), dtype=bool) { + placeholder.shared_1[threadIdx.x_2] = placeholder_1[(((((blockIdx.z*150) + (threadIdx.x_2*75)) + (rc.outer*25)) + (ry.outer*5)) + 2)] + } + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[threadIdx.x]*placeholder.shared_1[0])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[(threadIdx.x + 16)]*placeholder.shared_1[0])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[(threadIdx.x + 32)]*placeholder.shared_1[0])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[(threadIdx.x + 48)]*placeholder.shared_1[0])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[(threadIdx.x + 64)]*placeholder.shared_1[0])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[(threadIdx.x + 80)]*placeholder.shared_1[0])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[(threadIdx.x + 96)]*placeholder.shared_1[0])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[threadIdx.x]*placeholder.shared_1[1])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[(threadIdx.x + 16)]*placeholder.shared_1[1])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[(threadIdx.x + 32)]*placeholder.shared_1[1])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[(threadIdx.x + 48)]*placeholder.shared_1[1])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[(threadIdx.x + 64)]*placeholder.shared_1[1])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[(threadIdx.x + 80)]*placeholder.shared_1[1])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[(threadIdx.x + 96)]*placeholder.shared_1[1])) + attr [IterVar(threadIdx.z_1, (nullptr), "ThreadIndex", "threadIdx.z")] "thread_extent" = 1; + attr [IterVar(threadIdx.y_1, (nullptr), "ThreadIndex", "threadIdx.y")] "thread_extent" = 1; + attr [IterVar(threadIdx.x_1, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 16 { + pad_temp.shared_1[(threadIdx.x_1*7)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 447)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 1)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 446)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 2)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 445)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 3)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 444)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 4)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 443)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 5)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 442)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 6)] = @tir.if_then_else((((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)) && (((blockIdx.x*56) + floordiv(((threadIdx.x_1*7) + 9), 2)) < 113)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 441)], 0f32, dtype=float32) + } + attr [IterVar(threadIdx.z_2, (nullptr), "ThreadIndex", "threadIdx.z")] "thread_extent" = 1; + attr [IterVar(threadIdx.y_2, (nullptr), "ThreadIndex", "threadIdx.y")] "thread_extent" = 1; + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 16; + if @tir.likely((threadIdx.x_2 < 2), dtype=bool) { + placeholder.shared_1[threadIdx.x_2] = placeholder_1[(((((blockIdx.z*150) + (threadIdx.x_2*75)) + (rc.outer*25)) + (ry.outer*5)) + 3)] + } + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[threadIdx.x]*placeholder.shared_1[0])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[(threadIdx.x + 16)]*placeholder.shared_1[0])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[(threadIdx.x + 32)]*placeholder.shared_1[0])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[(threadIdx.x + 48)]*placeholder.shared_1[0])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[(threadIdx.x + 64)]*placeholder.shared_1[0])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[(threadIdx.x + 80)]*placeholder.shared_1[0])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[(threadIdx.x + 96)]*placeholder.shared_1[0])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[threadIdx.x]*placeholder.shared_1[1])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[(threadIdx.x + 16)]*placeholder.shared_1[1])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[(threadIdx.x + 32)]*placeholder.shared_1[1])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[(threadIdx.x + 48)]*placeholder.shared_1[1])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[(threadIdx.x + 64)]*placeholder.shared_1[1])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[(threadIdx.x + 80)]*placeholder.shared_1[1])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[(threadIdx.x + 96)]*placeholder.shared_1[1])) + attr [IterVar(threadIdx.z_1, (nullptr), "ThreadIndex", "threadIdx.z")] "thread_extent" = 1; + attr [IterVar(threadIdx.y_1, (nullptr), "ThreadIndex", "threadIdx.y")] "thread_extent" = 1; + attr [IterVar(threadIdx.x_1, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 16 { + pad_temp.shared_1[(threadIdx.x_1*7)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 446)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 1)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 445)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 2)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 444)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 3)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 443)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 4)] = @tir.if_then_else(((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 442)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 5)] = @tir.if_then_else((((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)) && (((blockIdx.x*56) + floordiv(((threadIdx.x_1*7) + 9), 2)) < 113)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 441)], 0f32, dtype=float32) + pad_temp.shared_1[((threadIdx.x_1*7) + 6)] = @tir.if_then_else((((2 <= (blockIdx.y + ry.outer)) && ((blockIdx.y + ry.outer) < 226)) && (((blockIdx.x*56) + floordiv((threadIdx.x_1*7), 2)) < 108)), placeholder[((((((rc.outer*50176) + (blockIdx.y*224)) + (ry.outer*224)) + (blockIdx.x*112)) + (threadIdx.x_1*7)) - 440)], 0f32, dtype=float32) + } + attr [IterVar(threadIdx.z_2, (nullptr), "ThreadIndex", "threadIdx.z")] "thread_extent" = 1; + attr [IterVar(threadIdx.y_2, (nullptr), "ThreadIndex", "threadIdx.y")] "thread_extent" = 1; + attr [IterVar(threadIdx.x_2, (nullptr), "ThreadIndex", "threadIdx.x")] "thread_extent" = 16; + if @tir.likely((threadIdx.x_2 < 2), dtype=bool) { + placeholder.shared_1[threadIdx.x_2] = placeholder_1[(((((blockIdx.z*150) + (threadIdx.x_2*75)) + (rc.outer*25)) + (ry.outer*5)) + 4)] + } + conv2d_nchw_1[0] = (conv2d_nchw_1[0] + (pad_temp.shared_1[threadIdx.x]*placeholder.shared_1[0])) + conv2d_nchw_1[2] = (conv2d_nchw_1[2] + (pad_temp.shared_1[(threadIdx.x + 16)]*placeholder.shared_1[0])) + conv2d_nchw_1[4] = (conv2d_nchw_1[4] + (pad_temp.shared_1[(threadIdx.x + 32)]*placeholder.shared_1[0])) + conv2d_nchw_1[6] = (conv2d_nchw_1[6] + (pad_temp.shared_1[(threadIdx.x + 48)]*placeholder.shared_1[0])) + conv2d_nchw_1[8] = (conv2d_nchw_1[8] + (pad_temp.shared_1[(threadIdx.x + 64)]*placeholder.shared_1[0])) + conv2d_nchw_1[10] = (conv2d_nchw_1[10] + (pad_temp.shared_1[(threadIdx.x + 80)]*placeholder.shared_1[0])) + conv2d_nchw_1[12] = (conv2d_nchw_1[12] + (pad_temp.shared_1[(threadIdx.x + 96)]*placeholder.shared_1[0])) + conv2d_nchw_1[1] = (conv2d_nchw_1[1] + (pad_temp.shared_1[threadIdx.x]*placeholder.shared_1[1])) + conv2d_nchw_1[3] = (conv2d_nchw_1[3] + (pad_temp.shared_1[(threadIdx.x + 16)]*placeholder.shared_1[1])) + conv2d_nchw_1[5] = (conv2d_nchw_1[5] + (pad_temp.shared_1[(threadIdx.x + 32)]*placeholder.shared_1[1])) + conv2d_nchw_1[7] = (conv2d_nchw_1[7] + (pad_temp.shared_1[(threadIdx.x + 48)]*placeholder.shared_1[1])) + conv2d_nchw_1[9] = (conv2d_nchw_1[9] + (pad_temp.shared_1[(threadIdx.x + 64)]*placeholder.shared_1[1])) + conv2d_nchw_1[11] = (conv2d_nchw_1[11] + (pad_temp.shared_1[(threadIdx.x + 80)]*placeholder.shared_1[1])) + conv2d_nchw_1[13] = (conv2d_nchw_1[13] + (pad_temp.shared_1[(threadIdx.x + 96)]*placeholder.shared_1[1])) + } + } + compute_1: Buffer(compute, float32, [501760], [])[((((blockIdx.z*100352) + (blockIdx.y*224)) + (blockIdx.x*112)) + threadIdx.x)] = max(conv2d_nchw_1[0], 0f32) + compute_1[(((((blockIdx.z*100352) + (blockIdx.y*224)) + (blockIdx.x*112)) + threadIdx.x) + 16)] = max(conv2d_nchw_1[2], 0f32) + compute_1[(((((blockIdx.z*100352) + (blockIdx.y*224)) + (blockIdx.x*112)) + threadIdx.x) + 32)] = max(conv2d_nchw_1[4], 0f32) + compute_1[(((((blockIdx.z*100352) + (blockIdx.y*224)) + (blockIdx.x*112)) + threadIdx.x) + 48)] = max(conv2d_nchw_1[6], 0f32) + compute_1[(((((blockIdx.z*100352) + (blockIdx.y*224)) + (blockIdx.x*112)) + threadIdx.x) + 64)] = max(conv2d_nchw_1[8], 0f32) + compute_1[(((((blockIdx.z*100352) + (blockIdx.y*224)) + (blockIdx.x*112)) + threadIdx.x) + 80)] = max(conv2d_nchw_1[10], 0f32) + compute_1[(((((blockIdx.z*100352) + (blockIdx.y*224)) + (blockIdx.x*112)) + threadIdx.x) + 96)] = max(conv2d_nchw_1[12], 0f32) + compute_1[(((((blockIdx.z*100352) + (blockIdx.y*224)) + (blockIdx.x*112)) + threadIdx.x) + 50176)] = max(conv2d_nchw_1[1], 0f32) + compute_1[(((((blockIdx.z*100352) + (blockIdx.y*224)) + (blockIdx.x*112)) + threadIdx.x) + 50192)] = max(conv2d_nchw_1[3], 0f32) + compute_1[(((((blockIdx.z*100352) + (blockIdx.y*224)) + (blockIdx.x*112)) + threadIdx.x) + 50208)] = max(conv2d_nchw_1[5], 0f32) + compute_1[(((((blockIdx.z*100352) + (blockIdx.y*224)) + (blockIdx.x*112)) + threadIdx.x) + 50224)] = max(conv2d_nchw_1[7], 0f32) + compute_1[(((((blockIdx.z*100352) + (blockIdx.y*224)) + (blockIdx.x*112)) + threadIdx.x) + 50240)] = max(conv2d_nchw_1[9], 0f32) + compute_1[(((((blockIdx.z*100352) + (blockIdx.y*224)) + (blockIdx.x*112)) + threadIdx.x) + 50256)] = max(conv2d_nchw_1[11], 0f32) + compute_1[(((((blockIdx.z*100352) + (blockIdx.y*224)) + (blockIdx.x*112)) + threadIdx.x) + 50272)] = max(conv2d_nchw_1[13], 0f32) + } +} +``` + +## 总结 + +本教程已经展示了如下内容: + +* 如何使用 TOPI API 操作 numpy 风格的算子。 +* TOPI 如何促进上下文的通用 schedule 和算子融合,来生成优化的内核代码。 + +[下载 Python 源代码:intro_topi.py](https://tvm.apache.org/docs/_downloads/3a9b1d387f618487c8ccf6b8b78ae179/intro_topi.py) + +[下载 Jupyter Notebook:intro_topi.ipynb](https://tvm.apache.org/docs/_downloads/63f9e50204143ea3c2d3593c72439b3d/intro_topi.ipynb) diff --git a/versioned_docs/version-0.12.0/tutorial/_category_.json b/versioned_docs/version-0.12.0/tutorial/_category_.json new file mode 100644 index 00000000..346153ae --- /dev/null +++ b/versioned_docs/version-0.12.0/tutorial/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 110 +} diff --git a/versioned_docs/version-0.12.0/tutorial_idx.md b/versioned_docs/version-0.12.0/tutorial_idx.md new file mode 100644 index 00000000..6ff99f6c --- /dev/null +++ b/versioned_docs/version-0.12.0/tutorial_idx.md @@ -0,0 +1,23 @@ +--- +title: 用户教程 +slug: /tutorial +sidebar_position: 3 +--- + +本教程旨在为刚接触 TVM 项目的用户提供介绍。它从 TVM 工作原理的基本信息开始,到安装 TVM、编译和优化模型,再到深入挖掘张量表达式语言以及基于它构建的调优工具。看完本教程后,新用户应该能够熟练使用 TVM 来优化模型,并能够更深入地研究 TVM。 + +本章内容概览: + +* [TVM 原理简介](/docs/tutorial/intro) +* [安装 TVM](/docs/tutorial/install) +* [使用 TVMC 编译和优化模型](/docs/tutorial/compile) +* [使用 TVMC Python 快速入门:TVM 的高级 API](/docs/tutorial/tvmc_python) +* [使用 Python 接口(AutoTVM)编译和优化模型](/docs/tutorial/python_AutoTVM) +* [使用张量表达式操作算子](/docs/tutorial/tensor_expr) +* [使用 Schedule 模板和 AutoTVM 优化算子](/docs/tutorial/ops_AutoTVM) +* [使用 Auto-scheduling 优化算子](/docs/tutorial/ops_AutoScheduling) +* [TensorIR 快速入门](/docs/tutorial/tensorIR) +* [交叉编译和 RPC](/docs/tutorial/rpc) +* [快速入门:编译深度学习模型](/docs/tutorial/quick_start) +* [利用 UMA 使硬件加速器可直接用于 TVM](/docs/tutorial/uma) +* [TOPI 简介](/docs/tutorial/TOPI) diff --git a/versioned_docs/version-0.12.0/user_guide/_category_.json b/versioned_docs/version-0.12.0/user_guide/_category_.json new file mode 100644 index 00000000..2d19c35b --- /dev/null +++ b/versioned_docs/version-0.12.0/user_guide/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "用户向导", + "position": 400 +} diff --git a/versioned_sidebars/version-0.12.0-sidebars.json b/versioned_sidebars/version-0.12.0-sidebars.json new file mode 100644 index 00000000..6ba57d11 --- /dev/null +++ b/versioned_sidebars/version-0.12.0-sidebars.json @@ -0,0 +1,156 @@ +{ + "tutorialSidebar": [ + "index", + { + "type": "category", + "label": "快速上手", + "items": [ + { + "type": "category", + "label": "安装 TVM", + "link": { + "type": "doc", + "id": "install-idx" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "install" + } + ] + }, + { + "type": "category", + "label": "贡献指南", + "link": { + "type": "doc", + "id": "contribute_idx" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "contribute" + } + ] + } + ], + "collapsible": false + }, + { + "type": "category", + "label": "用户手册", + "items": [ + { + "type": "category", + "label": "用户教程", + "link": { + "type": "doc", + "id": "tutorial_idx" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "tutorial" + } + ] + }, + { + "type": "category", + "label": "常见问题", + "link": { + "type": "doc", + "id": "how_to_idx" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "how_to" + } + ] + } + ], + "collapsible": false + }, + { + "type": "category", + "label": "开发手册", + "items": [ + { + "type": "category", + "label": "开发者教程", + "link": { + "type": "doc", + "id": "dev/index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "dev/tutorial" + } + ] + }, + { + "type": "category", + "label": "开发者指南", + "link": { + "type": "doc", + "id": "dev/how_to" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "dev/how_to" + } + ] + } + ], + "collapsible": false + }, + { + "type": "category", + "label": "设计与架构", + "items": [ + { + "type": "category", + "label": "设计与架构", + "link": { + "type": "doc", + "id": "arch/index" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "arch/arch" + } + ] + } + ], + "collapsible": false + }, + { + "type": "category", + "label": "主题指南", + "items": [ + { + "type": "doc", + "id": "topic/microtvm/index" + }, + { + "type": "category", + "label": "VTA:多功能张量加速器", + "link": { + "type": "doc", + "id": "topic/vta_idx" + }, + "items": [ + { + "type": "autogenerated", + "dirName": "topic/vta" + } + ] + } + ], + "collapsible": false + } + ] +} diff --git a/versions.json b/versions.json index d7be0f6e..8aafa054 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,4 @@ [ + "0.12.0", "0.10.0" ]