Shortcuts

定时器快速入门

Created On: Apr 01, 2021 | Last Updated: Jan 19, 2024 | Last Verified: Not Verified

在本教程中,我们将介绍`torch.utils.benchmark.Timer`的主要API。PyTorch计时器基于`timeit.Timer <https://docs.python.org/3/library/timeit.html#timeit.Timer>`__ API,并进行了多处PyTorch特定的修改。尽管对内置计时器类的熟悉不是本教程的前提,但我们假设读者了解性能工作的基础知识。

有关更全面的性能调优教程,请参见`PyTorch Benchmark <https://pytorch.org/tutorials/recipes/recipes/benchmark.html>`__。

内容:
  1. 定义计时器

  2. 墙上时间: Timer.blocked_autorange(…)

  3. C++代码段

  4. 指令计数: Timer.collect_callgrind(…)

  5. 指令计数: 深入研究

  6. 使用Callgrind进行A/B测试

  7. 总结

  8. 脚注

1. 定义计时器

`Timer`是一个任务定义工具。

from torch.utils.benchmark import Timer

timer = Timer(
    # The computation which will be run in a loop and timed.
    stmt="x * y",

    # `setup` will be run before calling the measurement loop, and is used to
    # populate any state which is needed by `stmt`
    setup="""
        x = torch.ones((128,))
        y = torch.ones((128,))
    """,

    # Alternatively, ``globals`` can be used to pass variables from the outer scope.
    #
    #    globals={
    #        "x": torch.ones((128,)),
    #        "y": torch.ones((128,)),
    #    },

    # Control the number of threads that PyTorch uses. (Default: 1)
    num_threads=1,
)

2. 墙上时间: Timer.blocked_autorange(...)

该方法将处理细节,如选择适当的重复次数、固定线程数,并提供结果的方便表示。

# Measurement objects store the results of multiple repeats, and provide
# various utility features.
from torch.utils.benchmark import Measurement

m: Measurement = timer.blocked_autorange(min_run_time=1)
print(m)
代码片段壁钟时间
     <torch.utils.benchmark.utils.common.Measurement object at 0x7f1929a38ed0>
     x * y
     setup:
       x = torch.ones((128,))
       y = torch.ones((128,))

       Median: 2.34 us
       IQR:    0.07 us (2.31 to 2.38)
       424 measurements, 1000 runs per measurement, 1 thread

3. C++代码片段

from torch.utils.benchmark import Language

cpp_timer = Timer(
    "x * y;",
    """
        auto x = torch::ones({128});
        auto y = torch::ones({128});
    """,
    language=Language.CPP,
)

print(cpp_timer.blocked_autorange(min_run_time=1))
C++代码片段壁钟时间
     <torch.utils.benchmark.utils.common.Measurement object at 0x7f192b019ed0>
     x * y;
     setup:
       auto x = torch::ones({128});
       auto y = torch::ones({128});

       Median: 1.21 us
       IQR:    0.03 us (1.20 to 1.23)
       83 measurements, 10000 runs per measurement, 1 thread

毫不意外的是,C++代码片段既更快又具有更低的变化。

4. 指令计数: Timer.collect_callgrind(...)

为了深入研究,Timer.collect_callgrind 包装了`Callgrind <https://valgrind.org/docs/manual/cl-manual.html>`__,以收集指令计数。这些计数非常有用,因为它们提供了对代码片段运行方式的细粒度和确定性(在Python中噪声非常低)见解。

from torch.utils.benchmark import CallgrindStats, FunctionCounts

stats: CallgrindStats = cpp_timer.collect_callgrind()
print(stats)
C++ Callgrind统计数据(摘要)
     <torch.utils.benchmark.utils.valgrind_wrapper.timer_interface.CallgrindStats object at 0x7f1929a35850>
     x * y;
     setup:
       auto x = torch::ones({128});
       auto y = torch::ones({128});

                             All          Noisy symbols removed
         Instructions:       563600                     563600
         Baseline:                0                          0
     100 runs per measurement, 1 thread

5. 指令计数: 更深入探索

``CallgrindStats``的字符串表示类似于Measurement。`噪声符号`是一个Python的概念(移除CPython解释器中被认为是噪声的调用)。

然而,对于更详细的分析,我们需要查看具体的调用。CallgrindStats.stats() 会返回``FunctionCounts``对象以方便分析。从概念上讲,``FunctionCounts``可以看作是一个带一些实用方法的键值对元组,其中每对是`(指令数,文件路径和函数名称)`。

关于路径的说明:

通常情况下,我们并不关心绝对路径。例如,乘法调用的完整路径和函数名称类似以下内容:

 /the/prefix/to/your/pytorch/install/dir/pytorch/build/aten/src/ATen/core/TensorMethods.cpp:at::Tensor::mul(at::Tensor const&) const [/the/path/to/your/conda/install/miniconda3/envs/ab_ref/lib/python3.7/site-packages/torch/lib/libtorch_cpu.so]

when in reality, all of the information that we're interested in can be
represented in:
 build/aten/src/ATen/core/TensorMethods.cpp:at::Tensor::mul(at::Tensor const&) const

``CallgrindStats.as_standardized()`` makes a best effort to strip low signal
portions of the file path, as well as the shared object and is generally
recommended.
inclusive_stats = stats.as_standardized().stats(inclusive=False)
print(inclusive_stats[:10])
C++ Callgrind统计数据(详细)
     torch.utils.benchmark.utils.valgrind_wrapper.timer_interface.FunctionCounts object at 0x7f192a6dfd90>
       47264  ???:_int_free
       25963  ???:_int_malloc
       19900  build/../aten/src/ATen/TensorIter ... (at::TensorIteratorConfig const&)
       18000  ???:__tls_get_addr
       13500  ???:malloc
       11300  build/../c10/util/SmallVector.h:a ... (at::TensorIteratorConfig const&)
       10345  ???:_int_memalign
       10000  build/../aten/src/ATen/TensorIter ... (at::TensorIteratorConfig const&)
        9200  ???:free
        8000  build/../c10/util/SmallVector.h:a ... IteratorBase::get_strides() const

     Total: 173472

这仍然有很多内容需要消化。我们可以使用`FunctionCounts.transform`方法修剪一些函数路径,并丢弃被调用的函数。当我们这样做时,任何碰撞的计数(例如,foo.h:a()foo.h:b() 都将映射到 foo.h)会被加起来。

import os
import re

def group_by_file(fn_name: str):
    if fn_name.startswith("???"):
        fn_dir, fn_file = fn_name.split(":")[:2]
    else:
        fn_dir, fn_file = os.path.split(fn_name.split(":")[0])
        fn_dir = re.sub("^.*build/../", "", fn_dir)
        fn_dir = re.sub("^.*torch/", "torch/", fn_dir)

    return f"{fn_dir:<15} {fn_file}"

print(inclusive_stats.transform(group_by_file)[:10])
Callgrind统计数据(简化版)
     <torch.utils.benchmark.utils.valgrind_wrapper.timer_interface.FunctionCounts object at 0x7f192995d750>
       118200  aten/src/ATen   TensorIterator.cpp
        65000  c10/util        SmallVector.h
        47264  ???             _int_free
        25963  ???             _int_malloc
        20900  c10/util        intrusive_ptr.h
        18000  ???             __tls_get_addr
        15900  c10/core        TensorImpl.h
        15100  c10/core        CPUAllocator.cpp
        13500  ???             malloc
        12500  c10/core        TensorImpl.cpp

     Total: 352327

6. 使用``Callgrind``进行A/B测试

指令计数最有用的特性之一是它们允许对计算进行细粒度的比较,这对于分析性能至关重要。

为了实际观察这一点,让我们比较两个大小为128的张量分别与 {128} x {1} 的乘法,该操作将广播第二个张量:

result = {a0 * b0, a1 * b0, …, a127 * b0}

broadcasting_stats = Timer(
    "x * y;",
    """
        auto x = torch::ones({128});
        auto y = torch::ones({1});
    """,
    language=Language.CPP,
).collect_callgrind().as_standardized().stats(inclusive=False)

通常我们希望在两个不同环境中进行A/B测试。(例如测试PR,或试验编译标志。)这非常简单,因为 CallgrindStats, FunctionCounts 和Measurement都是可序列化的。只需保存来自每个环境的测量数据,并在单个进程中加载它们以进行分析。

import pickle

# Let's round trip `broadcasting_stats` just to show that we can.
broadcasting_stats = pickle.loads(pickle.dumps(broadcasting_stats))


# And now to diff the two tasks:
delta = broadcasting_stats - inclusive_stats

def extract_fn_name(fn: str):
    """Trim everything except the function name."""
    fn = ":".join(fn.split(":")[1:])
    return re.sub(r"\(.+\)", "(...)", fn)

# We use `.transform` to make the diff readable:
print(delta.transform(extract_fn_name))
指令计数差异
     <torch.utils.benchmark.utils.valgrind_wrapper.timer_interface.FunctionCounts object at 0x7f192995d750>
         17600  at::TensorIteratorBase::compute_strides(...)
         12700  at::TensorIteratorBase::allocate_or_resize_outputs()
         10200  c10::SmallVectorImpl<long>::operator=(...)
          7400  at::infer_size(...)
          6200  at::TensorIteratorBase::invert_perm(...) const
          6064  _int_free
          5100  at::TensorIteratorBase::reorder_dimensions()
          4300  malloc
          4300  at::TensorIteratorBase::compatible_stride(...) const
           ...
           -28  _int_memalign
          -100  c10::impl::check_tensor_options_and_extract_memory_format(...)
          -300  __memcmp_avx2_movbe
          -400  at::detail::empty_cpu(...)
         -1100  at::TensorIteratorBase::numel() const
         -1300  void at::native::(...)
         -2400  c10::TensorImpl::is_contiguous(...) const
         -6100  at::TensorIteratorBase::compute_fast_setup_type(...)
        -22600  at::TensorIteratorBase::fast_set_up(...)

     Total: 58091

因此广播版本每次调用多消耗额外的580条指令(请记住,我们是每个样本收集100次运行),大约多了10%。有相当多的``TensorIterator``调用,所以我们深入分析这些。``FunctionCounts.filter``让这很简单。

print(delta.transform(extract_fn_name).filter(lambda fn: "TensorIterator" in fn))
指令计数差异(过滤后)
     <torch.utils.benchmark.utils.valgrind_wrapper.timer_interface.FunctionCounts object at 0x7f19299544d0>
         17600  at::TensorIteratorBase::compute_strides(...)
         12700  at::TensorIteratorBase::allocate_or_resize_outputs()
          6200  at::TensorIteratorBase::invert_perm(...) const
          5100  at::TensorIteratorBase::reorder_dimensions()
          4300  at::TensorIteratorBase::compatible_stride(...) const
          4000  at::TensorIteratorBase::compute_shape(...)
          2300  at::TensorIteratorBase::coalesce_dimensions()
          1600  at::TensorIteratorBase::build(...)
         -1100  at::TensorIteratorBase::numel() const
         -6100  at::TensorIteratorBase::compute_fast_setup_type(...)
        -22600  at::TensorIteratorBase::fast_set_up(...)

     Total: 24000

这清楚地表明发生了什么:在``TensorIterator``设置中存在快速路径,但在 {128} x {1} 的情况下,我们错过了这条路径,因此必须执行更为一般的分析,这更为昂贵。过滤器省略的最突出的调用是 c10::SmallVectorImpl<long>::operator=(…),这也是更一般设置的一部分。

7. 总结

总之,使用`Timer.blocked_autorange`收集壁钟时间。如果计时变化太大,请增加`min_run_time`,或者如果方便可以使用C++代码片段。

对于细粒度分析,使用`Timer.collect_callgrind`测量指令计数,并使用`FunctionCounts.(__add__ / __sub__ / transform / filter)`对它们进行切片和处理。

8. 脚注

  • 隐含``import torch``

    如果`globals`中不包含“torch”,Timer会自动填充它。这意味着``Timer(“torch.empty(())”)`` 可以工作。(不过其他导入应放在`setup`中,例如 Timer("np.zeros(())", "import numpy as np")

  • REL_WITH_DEB_INFO

    为了提供有关执行的PyTorch内部细节完整信息,``Callgrind``需要访问C++调试符号。这通过在构建PyTorch时设置``REL_WITH_DEB_INFO=1``实现。否则函数调用将是模糊的。(生成的``CallgrindStats``会在缺少调试符号的情况下发出警告。)

脚本的总运行时间: (0分钟 0.000秒)

由Sphinx-Gallery生成的图集

文档

访问 PyTorch 的详细开发者文档

查看文档

教程

获取针对初学者和高级开发人员的深入教程

查看教程

资源

查找开发资源并获得问题的解答

查看资源