0%

说明

因为项目有老代码所以才需要这样处理,一般建议用 RAII 等技术避免裸露资源。

find_malloc.sh

要点:

  1. 用 gcc 去除代码注释。
  2. 用 awk 对正则表达式计数。
check() {
  gcc -fpreprocessed -dD -E -P "$1" 2>/dev/null | awk -v file="$1" '
    BEGIN {
      malloc=0;
      free=0;
      queueCreate=0;
      queueDestroy=0;
      notifierCreate=0;
      notifierDestroy=0;
      handleCreate=0;
      handleDestroy=0;
    }
    /cnrtMalloc\(/          { malloc++; }
    /cnrtFree\(/            { free++; }
    /cnrtQueueCreate\(/     { queueCreate++; }
    /cnrtCreateQueue\(/     { queueCreate++; }
    /cnrtDestroyQueue\(/    { queueDestroy++; }
    /cnrtQueueDestroy\(/    { queueDestroy++; }
    /cnrtCreateNotifier\(/  { notifierCreate++; }
    /cnrtDestroyNotifier\(/ { notifierDestroy++; }
    /cnnlCreate\(/          { handleCreate++; }
    /cnnlDestroy\(/         { handleDestroy++; }
    END {
      if (malloc != free) {
        print "file: " file ", malloc: " malloc ", free: " free
      }
      if (queueCreate != queueDestroy) {
        print "file: " file ", queueCreate: " queueCreate ", queueDestroy: " queueDestroy
      }
      if (notifierCreate != notifierDestroy) {
        print "file: " file ", notifierCreate: " notifierCreate ", notifierDestroy: " notifierDestroy
      }
      if (handleCreate != handleDestroy) {
        print "file: " file ", handleCreate: " handleCreate ", handleDestroy: " handleDestroy
      }
    }'
}

check $1

find_malloc_all.sh

要点:使用 find 匹配时应该选择正则表达式类型,同时和 Python 的 re 模块一样要全字符串匹配(不能匹配只部分字符,因此想只匹配中间部分的时候,就要在两边加上 .*)。

OneDrive 可以同步桌面,我以前的桌面是 C:\Users\xxx\OneDrive\Desktop,现在我的新电脑的桌面是 C:\Users\xxx\Desktop,虽然 OneDrive 仍然保存了之前的文件夹,但是没有对桌面进行同步。

解决方案是从文件资源管理器进入用户目录,然后右键桌面,选择位置 > 移动。点击移动之后桌面上面的文件就会被移动到新的路径(新路径的旧文件不会被删除,遇到同名文件会询问是否覆盖),随后新的路径会被设置为桌面。

Tip

本文没有得到最终结论,只是一些个人猜想。

CUDA Kernel 常用 float 类型这件事 这篇笔记提到一个 issue 里面说 ATen 使用 double 作为 32 位浮点数的累加类型是因为用 float 会挂掉一个 batchnorm 的测试。这在我看来不可思议,因为 GPU 上面测试都没有挂,为什么 CPU 上面反而会挂呢?本文记录笔者看 batchnorm 的实现、试图找到原因的过程。

PyTorch 的 batchnorm 要对每一轮的输入计算均值和标准差,其中计算均值就需要将输入都加起来(再除以元素总数),这中间就可能产生累加误差。标准差的计算也类似,会有累加产生的误差。

从 aten/src/ATen/native/native_functions.yaml 中可以找到 native_batch_norm() 函数是在哪里实现的:

- func: native_batch_norm(Tensor input, Tensor? weight, Tensor? bias, Tensor? running_mean, Tensor? running_var, bool training, float momentum, float eps) -> (Tensor, Tensor, Tensor)
  dispatch:
    CPU: batch_norm_cpu
    CUDA: batch_norm_cuda
    MPS: batch_norm_mps
    MkldnnCPU: mkldnn_batch_norm

本文分别讨论双精度、单精度、半精度的浮点数计算,最后提及混合精度。在 CPU 方面,仅考虑 x86-64 CPU 和 GNU/Linux 上的 GCC 编译器;GPU 方面仅考虑 NVIDIA GPU。

GPU 上双精度计算慢在哪里?

https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#arithmetic-instructions 以上链接说明:GPU 双精度浮点数运算比单精度浮点数慢,在有些架构(很多 $x.y~(y \ne 0)$ 运算能力的 GPU 都是游戏卡)上甚至慢得多。除了指令慢之外,double 类型也不利于 cache 和全局内存带宽。

https://forums.developer.nvidia.com/t/use-float-rather-than-double-in-a-kernel/107363 A 64 bit double variable takes 2 registers. A 32 bit float can be stored in 1 register. 双精度浮点数的使用会增加单个 CUDA 线程对寄存器数量的需求,从而减少实际上同时可以运行的线程数。

怎么正确使用单精度类型?——谨防隐式转换成双精度

CUDA 和 C/C++ 都不会先将 float 转 double 再计算,除非……

https://godbolt.org/z/Me66bEeaa 我在本地尝试构造浮点数精度损失,但是无法构造出来,去 compiler explorer 一看发现实际上单精度浮点数被转换成了双精度浮点数计算。为什么呢?

https://godbolt.org/z/n5bjMGcx3

编译器并不会自动根据代码的依赖关系去编排静态初始化顺序,示例代码中 vec 在被推入两个元素之后又被初始化了一次(在 compiler explorer 中看汇编也能看出来)。

#include <iostream>
#include <vector>

extern std::vector<int> vec;

void report_size() {
    printf("vec.size(): %zd\n", vec.size());
}

auto _1 = []() {
    vec.push_back(0);
    printf("first  ");
    report_size();
    return 0;
}();

auto _2 = []() {
    vec.push_back(0);
    printf("second ");
    report_size();
    return 0;
}();

std::vector<int> vec;

int main() {
    printf("main   ");
    report_size();
}

输出:

first  vec.size(): 1
second vec.size(): 2
main   vec.size(): 0

2024 年 8 月 5 日:当前 torch 发布的版本是 2.4。PyTorch 的源码中还有几个 YAML 文件,这些文件都挺重要的,可以关注一下。

tools/autograd/derivatives.yaml 中有一些求导代码片段:

- name: prod.dim_int(Tensor self, int dim, bool keepdim=False, *, ScalarType? dtype=None) -> Tensor
  self: prod_backward(grad, self.to(grad.scalar_type()), result, dim, keepdim)
  result: (prod_backward(at::ones({}, result.options()).expand_as(result), self_p.to(result.scalar_type()), result, dim, keepdim) * self_t.conj()).sum(dim, keepdim).conj()

- name: put(Tensor self, Tensor index, Tensor source, bool accumulate=False) -> Tensor
  self: "accumulate ? grad : grad.put(index, zeros_like(source), false)"
  index: non_differentiable
  source: grad.take(index).reshape_as(source)
  result: self_t.put(index, source_t, accumulate)

- name: linalg_qr(Tensor A, str mode='reduced') -> (Tensor Q, Tensor R)
  A: linalg_qr_backward(grad_Q, grad_R, Q, R, mode)
  Q, R: linalg_qr_jvp(A_t, Q, R, mode)

- name: rad2deg(Tensor self) -> Tensor
  self: rad2deg_backward(grad)
  result: auto_element_wise

- name: random_.from(Tensor(a!) self, int from, int? to, *, Generator? generator=None) -> Tensor(a!)
  self: zeros_like(grad)
  result: self_t.zero_()

- name: random_.to(Tensor(a!) self, int to, *, Generator? generator=None) -> Tensor(a!)
  self: zeros_like(grad)
  result: self_t.zero_()

看起来 name 是近似于 python 的伪代码,其他的都是 C++ 代码?

aten/src/ATen/native/native_functions.yaml 这个文件中有 native 函数在不同平台上的派发,比如:

有的时候我们希望私有化构造函数,然后要求用户只通过工厂方法访问我们的类型,在 std::enable_shared_from_this 的例子 中就有使用。这个例子是从 cppreference 上面抄来的。但是今天我发现去年 11 月有人修改了网页上的例子,修订记录为 https://en.cppreference.com/mwiki/index.php?title=cpp%2Fmemory%2Fenable_shared_from_this&diff=162885&oldid=153414

后来这个例子又有了新的修订,现在这个例子是:

class Best : public std::enable_shared_from_this<Best>
{
    struct Private{ explicit Private() = default; }; // 这个 explicit 构造函数非常重要
 
public:
    // Constructor is only usable by this class
    Best(Private) {}
 
    // Everyone else has to use this factory function
    // Hence all Best objects will be contained in shared_ptr
    static std::shared_ptr<Best> create()
    {
        return std::make_shared<Best>(Private());
    }
 
    std::shared_ptr<Best> getptr()
    {
        return shared_from_this();
    }
};

构造函数不再私有,调用者能明确看到需要一个私有的标记类,从而对 API 的使用方式会更加清楚。

尤其是 Private 类中的 explicit Private() = default; 非常重要,如果没有这个,使用者可以通过避免明确书写 Private 类型而构造出 Private 类型的对象!这是非常危险的。

聊天记录:

A torch 的分布式程序在一些异常结束的情况下会留下一些僵尸进程 我之前经常是这样 你 kill 掉主进程子进程不会被回收

B 为什么没有会回收?因为父进程没死且没有被回收,难道父进程不是 torch 的主进程,而是整个容器里面的一个活跃进程? 不在容器里使用是否不会出现这种情况? 如果父进程死了,应该由 init 回收 是否是因为容器的起始进程不是 init

A 不知道

B 是在容器中吗

A 不是

关联:

论坛帖子 PyTorch doesn’t free GPU’s memory of it gets aborted due to out-of-memory error - PyTorch Forums 有 PyTorch 开发者回答是 Python 的 multiprocessing 模块有 bug,可能会导致僵尸进程。

参考

https://stackoverflow.com/a/59314670/

https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html 的解释:

Specifies the build type on single-configuration generators (e.g. Makefile Generators or Ninja). Typical values include DebugReleaseRelWithDebInfo and MinSizeRel, but custom build types can also be defined.

Stack Overflow 回答里提到的 BetaTest 等其他构建类型应该就是上面所说的 custom build types.

在尽可能复现错误的情况下调试

使用 RelWithDebInfo?

考虑编译器 gcc 或者 clang: