• <xmp id="om0om">
  • <table id="om0om"><noscript id="om0om"></noscript></table>
  • 3 月 19 日下午 2 點,鎖定 NVIDIA AI 網絡中文專場。立即注冊觀看
    數據科學

    調試混合 Python 和 C 語言堆棧

    調試很困難。跨多種語言調試尤其具有挑戰性,跨設備調試通常需要一個具有不同技能和專業知識的團隊來揭示潛在問題

    然而,項目通常需要使用多種語言,以確保必要時的高性能、用戶友好的體驗以及可能的兼容性。不幸的是,沒有一種編程語言能夠提供上述所有功能,這就要求開發人員變得多才多藝。

    這篇文章展示了RAPIDS該團隊著手調試多種編程語言,包括使用GDB以識別和解決死鎖。該團隊致力于設計加速和擴展數據科學解決方案的軟件。

    這篇文章中的 bug 是RAPIDS 項目這一問題在 2019 年夏天得到了確認和解決。它涉及到一個包含多種編程語言的復雜堆棧,主要是 C 、 C ++和 Python ,以及CUDA對于 GPU 加速度

    記錄這個歷史錯誤及其解決方案有幾個目的,包括:

    1. 用 GDB 演示 Python 和 C 調試
    2. 提出關于如何診斷死鎖的想法
    3. 更好地理解混合 Python 和 CUDA

    這篇文章中的內容應該有助于你理解這些錯誤是如何表現出來的,以及如何在你自己的工作中解決類似的問題。

    Bug 描述

    為了高效和高性能, RAPIDS 依賴于各種庫進行多種不同的操作。舉幾個例子, RAPIDS 使用CuPycuDF以分別計算 GPU 上的數組和數據幀。Numba是一個即時編譯器,可用于加速 GPU 上用戶定義的 Python 操作

    此外Dask用于將計算擴展到多個 GPU 和多個節點。手頭的 bug 中的最后一塊拼圖是UCX, a communication framework used to leverage a variety of interconnects , such as InfiniBand and NVLink .

    圖 1 顯示了該堆棧的概述。盡管當時未知,但該堆棧中的某個位置發生了死鎖,導致工作流無法完成。

    Diagram showing the relevant RAPIDS software stack
    圖 1 。 RAPIDS 和 Dask 集群中的組件堆棧

    這種僵局首次出現在 2019 年 8 月,也就是 UCX 引入堆棧后不久。事實證明,死鎖以前在沒有 UCX 的情況下表現出來(使用 Dask 默認的 TCP 通信器),只是偶爾出現。

    死鎖發生時,我們花了很多時間探索這個空間。盡管當時未知,但該錯誤可能發生在特定的操作中,例如group by aggregation,merge/joins,repartitioning,或在任何庫的特定版本中,包括 cuDF 、 CuPy 、 Dask 、 UCX 等。因此,有許多方面需要探索。

    準備調試

    接下來的部分將向您介紹如何為調試做準備。

    設置最小復制機

    找到一個最小的復制器是調試任何東西的關鍵。這個問題最初是在運行 8 GPU 的工作流中發現的。隨著時間的推移,我們將其減少到兩個 GPU 。擁有一個最小的復制器對于輕松地與他人共享錯誤并獲得更廣泛團隊的時間和關注至關重要。

    設置您的環境

    在深入研究這個問題之前,先設置好你的環境。 0 . 10 版本的 RAPIDS (于 2019 年 10 月發布)可以最低限度地再現該漏洞。可以使用 Conda 或 Docker 來設置環境(請參閱本文后面的相應部分)。

    整個過程假設使用 Linux 。由于 UCX 在 Windows 或 MacOS 上不受支持,因此在這些操作系統上無法復制。

    Conda

    首先,安裝Miniconda。初次安裝后,強烈建議您安裝mamba通過運行以下腳本:

    conda install mamba -n base -c conda-forge

    然后運行以下腳本創建并激活一個 RAPIDS 0 . 10 的 conda 環境:

    mamba create -n rapids-0.10 -c rapidsai -c nvidia -c conda-forge rapids=0.10 glog=0.4 cupy=6.7 numba=0.45.1 ucx-py=0.11 ucx=1.7 ucx-proc=*=gpu libnuma dask=2.30 dask-core=2.30 distributed=2.30 gdb
    conda activate rapids-0.10

    我們建議曼巴加快環境分辨率。跳過該步驟并替換mamba具有conda應該也能工作,但可能會慢得多。

    Docker

    或者,您可以使用 Docker 重現該錯誤。在你擁有NVIDIA Container Toolkit之后按照這些說明進行設置。

    docker run -it --rm --cap-add sys_admin --cap-add sys_ptrace --ipc shareable --net host --gpus all rapidsai/rapidsai:0.10-cuda10.0-runtime-ubuntu18.04 /bin/bash

    在容器中,安裝mamba以加快環境分辨率。

    conda create -n mamba -c conda-forge mamba -y

    然后,安裝 UCX / UCX-Py ,然后libnuma,這是一個 UCX 依賴項。此外,將 Dask 升級到集成了 UCX 支持的版本。為了以后進行調試,還可以安裝 GDB 。

    /opt/conda/envs/mamba/bin/mamba install -y -c rapidsai -c nvidia -c conda-forge dask=2.30 dask-core=2.30 distributed=2.30 fsspec=2022.11.0 libnuma ucx-py=0.11 ucx=1.7 ucx-proc=*=gpu gdb -p /opt/conda/envs/rapids

    調試

    本節詳細介紹了這個特定問題是如何遇到并最終解決的,并提供了詳細的分步概述。您還可以復制和練習一些所描述的概念。

    正在運行(或掛起)

    有問題的調試問題肯定不僅限于單個計算問題,但使用我們在 2019 年使用的相同工作流更容易。可以通過運行以下腳本將該腳本下載到本地環境:

    wget
    https://gist.githubusercontent.com/pentschev/9ce97f8efe370552c7dd5e84b64d3c92/raw/424c9cf95f31c18d32a9481f78dd241e08a071a9/cudf-deadlock.py

    要進行復制,請執行以下操作:

    OPENBLAS_NUM_THREADS=1 UCX_RNDV_SCHEME=put_zcopy UCX_MEMTYPE_CACHE=n UCX_TLS=sockcm,tcp,cuda_copy,cuda_ipc python cudf-deadlock.py

    在幾次迭代中(可能只有一兩次),您應該會看到前面的程序掛起。現在真正的工作開始了。

    僵局

    死鎖的一個好特性是進程和線程(如果你知道如何調查它們)可以顯示它們當前正在嘗試做什么。你可以推斷出是什么導致了死鎖

    關鍵工具是 GDB 。然而, PDB 最初花了很多時間來調查 Python 在每一步都在做什么。 GDB 可以連接到活動進程,因此您必須首先了解進程及其關聯 ID 是什么:

    (rapids) root@dgx13:/rapids/notebooks# ps ax | grep python
       19 pts/0    S      0:01 /opt/conda/envs/rapids/bin/python /opt/conda/envs/rapids/bin/jupyter-lab --allow-root --ip=0.0.0.0 --no-browser --NotebookApp.token=
      865 pts/0    Sl+    0:03 python cudf-deadlock.py
      871 pts/0    S+     0:00 /opt/conda/envs/rapids/bin/python -c from multiprocessing.semaphore_tracker import main;main(69)
      873 pts/0    Sl+    0:08 /opt/conda/envs/rapids/bin/python -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=70, pipe_handle=76) --multiprocessing-fork
      885 pts/0    Sl+    0:07 /opt/conda/envs/rapids/bin/python -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=70, pipe_handle=85) --multiprocessing-fork

    四個 Python 過程與此問題相關:

    • Dask 客戶端 (865)
    • Dask 調度程序 (871)
    • 兩名 Dask 工人 (873885)

    有趣的是,自從最初調查這個錯誤以來,在調試 Python 方面已經取得了重大進展。 2019 年, RAPIDS 在 Python 3 . 6 上運行,該版本已經有了調試較低堆棧的工具,但只有當 Python 以調試模式構建時。這可能需要重建整個軟件堆棧,這在像這樣的復雜情況下是令人望而卻步的

    由于 Python 3 . 8debug builds use the same ABI as release builds,極大地簡化了 C 和 Python 堆棧組合的調試。我們在這篇文章中沒有涉及到這一點。

    GDB 勘探

    使用gdb連接到最后一個正在運行的進程( Dask 工人之一):

    (rapids) root@dgx13:/rapids/notebooks# gdb -p 885
    Attaching to process 885
    [New LWP 889]
    [New LWP 890]
    [New LWP 891]
    [New LWP 892]
    [New LWP 893]
    [New LWP 894]
    [New LWP 898]
    [New LWP 899]
    [New LWP 902]
    [Thread debugging using libthread_db enabled]
    Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
    0x00007f5494d48938 in pthread_rwlock_wrlock () from /lib/x86_64-linux-gnu/libpthread.so.0
    (gdb)

    每個 Dask 工作程序都有幾個線程(通信、計算、管理等等)。使用gdb命令info threads檢查每個線程在做什么。

    (gdb) info threads
      Id   Target Id                                        Frame
    * 1    Thread 0x7f5495177740 (LWP 885) "python"         0x00007f5494d48938 in pthread_rwlock_wrlock () from /lib/x86_64-linux-gnu/libpthread.so.0
      2    Thread 0x7f5425b98700 (LWP 889) "python"         0x00007f5494d4d384 in read () from /lib/x86_64-linux-gnu/libpthread.so.0
      3    Thread 0x7f5425357700 (LWP 890) "python"         0x00007f5494d49f85 in pthread_cond_timedwait@@GLIBC_2.3.2 () from /lib/x86_64-linux-gnu/libpthread.so.0
      4    Thread 0x7f5424b16700 (LWP 891) "python"         0x00007f5494d49f85 in pthread_cond_timedwait@@GLIBC_2.3.2 () from /lib/x86_64-linux-gnu/libpthread.so.0
      5    Thread 0x7f5411fff700 (LWP 892) "cuda-EvtHandlr" 0x00007f5494a5fbf9 in poll () from /lib/x86_64-linux-gnu/libc.so.6
      6    Thread 0x7f54117fe700 (LWP 893) "python"         0x00007f5494a6cbb7 in epoll_wait () from /lib/x86_64-linux-gnu/libc.so.6
      7    Thread 0x7f5410d3c700 (LWP 894) "python"         0x00007f5494d4c6d6 in do_futex_wait.constprop () from /lib/x86_64-linux-gnu/libpthread.so.0
      8    Thread 0x7f53f6048700 (LWP 898) "python"         0x00007f5494d49f85 in pthread_cond_timedwait@@GLIBC_2.3.2 () from /lib/x86_64-linux-gnu/libpthread.so.0
      9    Thread 0x7f53f5847700 (LWP 899) "cuda-EvtHandlr" 0x00007f5494a5fbf9 in poll () from /lib/x86_64-linux-gnu/libc.so.6
      10   Thread 0x7f53a39d9700 (LWP 902) "python"         0x00007f5494d4c6d6 in do_futex_wait.constprop () from /lib/x86_64-linux-gnu/libpthread.so.0

    這個 Dask 工作程序有 10 個線程,其中一半似乎在等待互斥/ futex 。另一半cuda-EvtHandlr正在輪詢。通過查看回溯,觀察當前線程(由左側的*表示)線程 1 正在做什么:

    (gdb) bt
    #0  0x00007f5494d48938 in pthread_rwlock_wrlock () from /lib/x86_64-linux-gnu/libpthread.so.0
    #1  0x00007f548bc770a8 in ?? () from /usr/lib/x86_64-linux-gnu/libcuda.so
    #2  0x00007f548ba3d87c in ?? () from /usr/lib/x86_64-linux-gnu/libcuda.so
    #3  0x00007f548bac6dfa in ?? () from /usr/lib/x86_64-linux-gnu/libcuda.so
    #4  0x00007f54240ba372 in uct_cuda_ipc_iface_event_fd_arm (tl_iface=0x562398656990, events=<optimized out>) at cuda_ipc/cuda_ipc_iface.c:271
    #5  0x00007f54241d4fc2 in ucp_worker_arm (worker=0x5623987839e0) at core/ucp_worker.c:1990
    #6  0x00007f5424259b76 in __pyx_pw_3ucp_5_libs_4core_18ApplicationContext_23_blocking_progress_mode_1_fd_reader_callback ()
       from /opt/conda/envs/rapids/lib/python3.6/site-packages/ucp/_libs/core.cpython-36m-x86_64-linux-gnu.so
    #7  0x000056239601d5ae in PyObject_Call (func=<cython_function_or_method at remote 0x7f54242bb608>, args=<optimized out>, kwargs=<optimized out>)
        at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Objects/abstract.c:2261
    #8  0x00005623960d13a2 in do_call_core (kwdict=0x0, callargs=(), func=<cython_function_or_method at remote 0x7f54242bb608>)
        at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Python/ceval.c:5120
    #9  _PyEval_EvalFrameDefault (f=<optimized out>, throwflag=<optimized out>) at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Python/ceval.c:3404
    #10 0x00005623960924b5 in PyEval_EvalFrameEx (throwflag=0, f=Python Exception <class 'RuntimeError'> Type does not have a target.:
    ) at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Python/ceval.c:754
    #11 _PyFunction_FastCall (globals=<optimized out>, nargs=<optimized out>, args=<optimized out>, co=<optimized out>)
        at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Python/ceval.c:4933
    #12 fast_function (func=<optimized out>, stack=<optimized out>, nargs=<optimized out>, kwnames=<optimized out>)
        at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Python/ceval.c:4968
    #13 0x00005623960a13af in call_function (pp_stack=0x7ffdfa2311e8, oparg=<optimized out>, kwnames=0x0)
        at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Python/ceval.c:4872
    #14 0x00005623960cfcaa in _PyEval_EvalFrameDefault (f=<optimized out>, throwflag=<optimized out>)
        at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Python/ceval.c:3335
    #15 0x00005623960924b5 in PyEval_EvalFrameEx (throwflag=0, Python Exception <class 'RuntimeError'> Type does not have a target.:
    f=) at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Python/ceval.c:754
    #16 _PyFunction_FastCall (globals=<optimized out>, nargs=<optimized out>, args=<optimized out>, co=<optimized out>)
        at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Python/ceval.c:4933
    #17 fast_function (func=<optimized out>, stack=<optimized out>, nargs=<optimized out>, kwnames=<optimized out>)
        at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Python/ceval.c:4968
    #18 0x00005623960a13af in call_function (pp_stack=0x7ffdfa2313f8, oparg=<optimized out>, kwnames=0x0)
        at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Python/ceval.c:4872
    #19 0x00005623960cfcaa in _PyEval_EvalFrameDefault (f=<optimized out>, throwflag=<optimized out>)
        at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Python/ceval.c:3335
    #20 0x00005623960924b5 in PyEval_EvalFrameEx (throwflag=0, Python Exception <class 'RuntimeError'> Type does not have a target.:
    f=) at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Python/ceval.c:754

    查看堆棧的前 20 幀(為了簡潔起見,后面的幀都是不相關的 Python 內部調用,省略了),您可以看到一些內部的 Python 調用:_PyEval_EvalFrameDefault,_PyFunction_FastCall_PyEval_EvalCodeWithName。也有一些電話libcuda.so.

    這一觀察結果暗示可能存在死鎖。它可以是 Python 、 CUDA ,也可能是兩者都有。這個Linux Wikibook on Deadlocks包含調試死鎖的方法,以幫助您向前邁進

    然而pthread_mutex_lock正如維基解密中所描述的,它在這里pthread_rwlock_wrlock.

    (gdb) bt
    #0  0x00007f8e94762938 in pthread_rwlock_wrlock () from /lib/x86_64-linux-gnu/libpthread.so.0
    #1  0x00007f8e8b6910a8 in ?? () from /usr/lib/x86_64-linux-gnu/libcuda.so
    #2  0x00007f8e8b45787c in ?? () from /usr/lib/x86_64-linux-gnu/libcuda.so

    根據documentation for pthread_rwlock_wrlock,只需要一個參數,rwlock,這是一個讀/寫鎖。現在,看看代碼在做什么,并列出源代碼:

    (gdb) list
    6       /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Programs/python.c: No such file or directory.

    沒有調試符號。回到 Linux Wikibook ,您可以查看寄存器。您也可以在 GDB 中這樣做:

    (gdb) info reg
    rax            0xfffffffffffffe00  -512
    rbx            0x5623984aa750      94710878873424
    rcx            0x7f5494d48938      140001250937144
    rdx            0x3                 3
    rsi            0x189               393
    rdi            0x5623984aa75c      94710878873436
    rbp            0x0                 0x0
    rsp            0x7ffdfa230be0      0x7ffdfa230be0
    r8             0x0                 0
    r9             0xffffffff          4294967295
    r10            0x0                 0
    r11            0x246               582
    r12            0x5623984aa75c      94710878873436
    r13            0xca                202
    r14            0xffffffff          4294967295
    r15            0x5623984aa754      94710878873428
    rip            0x7f5494d48938      0x7f5494d48938 <pthread_rwlock_wrlock+328>
    eflags         0x246               [ PF ZF IF ]
    cs             0x33                51
    ss             0x2b                43
    ds             0x0                 0
    es             0x0                 0
    fs             0x0                 0
    gs             0x0                 0

    問題是不知道它們的意思。幸運的是,文檔是存在的,例如Guide to x86-64 from Stanford CS107,解釋了前六個參數在寄存器中%rdi,%rsi,%rdx,%rcx,%r8%r9.

    如前所述,pthread_rwlock_wrlock只需要一個參數,所以必須在%rdi剩下的可能會被用作通用寄存器pthread_rwlock_wrlock.

    現在,您需要閱讀%rdi登記你已經知道它有一個類pthread_rwlock_t,因此必須可以取消引用:

    (gdb) p *(pthread_rwlock_t*)$rdi
    $2 = {__data = {__lock = 3, __nr_readers = 0, __readers_wakeup = 0, __writer_wakeup = 898, __nr_readers_queued = 0, __nr_writers_queued = 0, __writer = 0,
        __shared = 0, __pad1 = 0, __pad2 = 0, __flags = 0}, __size = "\003", '\000' <repeats 11 times>, "\202\003", '\000' <repeats 41 times>, __align = 3}

    顯示的是pthread_rwlock_t反對libcuda.so傳遞給pthread_rwlock_wrlock– 鎖本身。不幸的是,這些名字并沒有太大的相關性。你可以推斷__lock可能意味著同時嘗試獲取鎖的次數,但這是推斷的范圍

    唯一具有非零值的其他屬性是__write_wakeup。 Linux Wikibook 列出了一個有趣的值,稱為__owner,它指向當前擁有鎖所有權的進程標識符( PID )。鑒于此pthread_rwlock_t是讀/寫鎖,假設__writer_wakeup指向擁有鎖的進程可能是一個很好的下一步。

    關于 Linux 的一個事實是,程序中的每個線程都像一個進程一樣運行。每個線程都應該有一個 PID (或 GDB 中的 LWP )

    再次查看進程中的所有線程,查找一個 PID 與相同的線程__writer_wakeup。幸運的是,有一個線程確實具有該 ID :

    (gdb) info threads
      Id   Target Id                                        Frame
      8    Thread 0x7f53f6048700 (LWP 898) "python"         0x00007f5494d49f85 in pthread_cond_timedwait@@GLIBC_2.3.2 () from /lib/x86_64-linux-gnu/libpthread.so.0

    到目前為止,線程 8 可能擁有線程 1 試圖獲取的鎖。線程 8 的堆棧可能會提供有關正在發生的事情的線索。接下來運行:

    (gdb) thread apply 8 bt
    Thread 8 (Thread 0x7f53f6048700 (LWP 898) "python"):
    #0  0x00007f5494d49f85 in pthread_cond_timedwait@@GLIBC_2.3.2 () from /lib/x86_64-linux-gnu/libpthread.so.0
    #1  0x00005623960e59e0 in PyCOND_TIMEDWAIT (cond=0x562396232f40 <gil_cond>, mut=0x562396232fc0 <gil_mutex>, us=5000) at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Python/condvar.h:103
    #2  take_gil (tstate=0x5623987ff240) at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Python/ceval_gil.h:224
    #3  0x000056239601cf7e in PyEval_RestoreThread (tstate=0x5623987ff240) at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Python/ceval.c:369
    #4  0x00005623960e5cd4 in PyGILState_Ensure () at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Python/pystate.c:895
    #5  0x00007f5493610aa7 in _CallPythonObject (pArgs=0x7f53f6042e80, flags=4353, converters=(<_ctypes.PyCSimpleType at remote 0x562396b4d588>,), callable=<function at remote 0x7f53ec6e6950>, setfunc=0x7f549360ba80 <L_set>, restype=0x7f549369b9d8, mem=0x7f53f6043010) at /usr/local/src/conda/python-3.6.11/Modules/_ctypes/callbacks.c:141
    #6  closure_fcn (cif=<optimized out>, resp=0x7f53f6043010, args=0x7f53f6042e80, userdata=<optimized out>) at /usr/local/src/conda/python-3.6.11/Modules/_ctypes/callbacks.c:296
    #7  0x00007f54935fa3d0 in ffi_closure_unix64_inner () from /opt/conda/envs/rapids/lib/python3.6/lib-dynload/../../libffi.so.6
    #8  0x00007f54935fa798 in ffi_closure_unix64 () from /opt/conda/envs/rapids/lib/python3.6/lib-dynload/../../libffi.so.6
    #9  0x00007f548ba99dc6 in ?? () from /usr/lib/x86_64-linux-gnu/libcuda.so
    #10 0x00007f548badd4a5 in ?? () from /usr/lib/x86_64-linux-gnu/libcuda.so
    #11 0x00007f54935fa630 in ffi_call_unix64 () from /opt/conda/envs/rapids/lib/python3.6/lib-dynload/../../libffi.so.6
    #12 0x00007f54935f9fed in ffi_call () from /opt/conda/envs/rapids/lib/python3.6/lib-dynload/../../libffi.so.6
    #13 0x00007f549361109e in _call_function_pointer (argcount=6, resmem=0x7f53f6043400, restype=<optimized out>, atypes=0x7f53f6043380, avalues=0x7f53f60433c0, pProc=0x7f548bad61f0 <cuOccupancyMaxPotentialBlockSize>, flags=4353) at /usr/local/src/conda/python-3.6.11/Modules/_ctypes/callproc.c:831
    #14 _ctypes_callproc (pProc=0x7f548bad61f0 <cuOccupancyMaxPotentialBlockSize>, argtuple=<optimized out>, flags=4353, argtypes=<optimized out>, restype=<_ctypes.PyCSimpleType at remote 0x562396b4d588>, checker=0x0) at /usr/local/src/conda/python-3.6.11/Modules/_ctypes/callproc.c:1195
    #15 0x00007f5493611ad5 in PyCFuncPtr_call (self=self@entry=0x7f53ed534750, inargs=<optimized out>, kwds=<optimized out>) at /usr/local/src/conda/python-3.6.11/Modules/_ctypes/_ctypes.c:3970
    #16 0x000056239601d5ae in PyObject_Call (func=Python Exception <class 'RuntimeError'> Type does not have a target.:
    , args=<optimized out>, kwargs=<optimized out>) at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Objects/abstract.c:2261
    #17 0x00005623960d13a2 in do_call_core (kwdict=0x0, callargs=(<CArgObject at remote 0x7f53ed516530>, <CArgObject at remote 0x7f53ed516630>, <c_void_p at remote 0x7f53ed4cad08>, <CFunctionType at remote 0x7f5410f4ef20>, 0, 1024), func=Python Exception <class 'RuntimeError'> Type does not have a target.:
    ) at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Python/ceval.c:5120
    #18 _PyEval_EvalFrameDefault (f=<optimized out>, throwflag=<optimized out>) at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Python/ceval.c:3404
    #19 0x0000562396017ea8 in PyEval_EvalFrameEx (throwflag=0, f=Python Exception <class 'RuntimeError'> Type does not have a target.:
    ) at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Python/ceval.c:754
    #20 _PyEval_EvalCodeWithName (_co=<optimized out>, globals=<optimized out>, locals=<optimized out>, args=<optimized out>, argcount=<optimized out>, kwnames=0x0, kwargs=0x7f541805a390, kwcount=<optimized out>, kwstep=1, defs=0x0, defcount=0, kwdefs=0x0, closure=(<cell at remote 0x7f5410520408>, <cell at remote 0x7f53ed637c48>, <cell at remote 0x7f53ed6377f8>), name=Python Exception <class 'RuntimeError'> Type does not have a target.:
    , qualname=Python Exception <class 'RuntimeError'> Type does not have a target.:
    ) at /home/conda/feedstock_root/build_artifacts/python_1596656032113/work/Python/ceval.c:4166

    在堆棧的頂部,它看起來像是一個普通的 Python 線程在等待GIL。它看起來不起眼,所以你可以忽略它,在其他地方尋找線索。這正是我們在 2019 年所做的

    更全面地查看堆棧的其余部分,尤其是第 9 幀和第 10 幀:

    #9  0x00007f548ba99dc6 in ?? () from /usr/lib/x86_64-linux-gnu/libcuda.so
    #10 0x00007f548badd4a5 in ?? () from /usr/lib/x86_64-linux-gnu/libcuda.so

    在這一點上,事情可能看起來更加令人困惑。線程 1 正在鎖定libcuda.so內部構件。如果不能訪問 CUDA 源代碼,調試將很困難

    進一步檢查 Thread 8 的堆棧,可以看到兩個提供提示的幀:

    #13 0x00007f549361109e in _call_function_pointer (argcount=6, resmem=0x7f53f6043400, restype=<optimized out>, atypes=0x7f53f6043380, avalues=0x7f53f60433c0, pProc=0x7f548bad61f0 <cuOccupancyMaxPotentialBlockSize>, flags=4353) at /usr/local/src/conda/python-3.6.11/Modules/_ctypes/callproc.c:831
    #14 _ctypes_callproc (pProc=0x7f548bad61f0 <cuOccupancyMaxPotentialBlockSize>, argtuple=<optimized out>, flags=4353, argtypes=<optimized out>, restype=<_ctypes.PyCSimpleType at remote 0x562396b4d588>, checker=0x0) at /usr/local/src/conda/python-3.6.11/Modules/_ctypes/callproc.c:1195

    綜上所述,兩個線程共享一個鎖。線程 8 正在嘗試獲取 GIL ,并對進行 CUDA 調用cuOccupancyMaxPotentialBlockSize.

    然而libcuda.so對 Python 一無所知,那么它為什么要試圖獲得 GIL 呢?

    的文檔cuOccupancyMaxPotentialBlockSize顯示它需要回調。回調是可以向另一個函數注冊的函數,以便在某個時間點執行,從而在該預定義點有效地執行用戶定義的操作。

    這很有趣。接下來,找出那個電話是從哪里打來的。通過一堆又一堆的代碼—— cuDF 、 Dask 、 RMM 、 CuPy 和 Numba ,可以顯式調用cuOccupancyMaxPotentialBlockSize在 0 . 45 版本的 Numba 中:

    def get_max_potential_block_size(self, func, b2d_func, memsize, blocksizelimit, flags=None):
        """Suggest a launch configuration with reasonable occupancy.
        :param func: kernel for which occupancy is calculated
        :param b2d_func: function that calculates how much per-block dynamic shared memory 'func' uses based on the block size.
        :param memsize: per-block dynamic shared memory usage intended, in bytes
        :param blocksizelimit: maximum block size the kernel is designed to handle"""
     
     
        gridsize = c_int()
        blocksize = c_int()
        b2d_cb = cu_occupancy_b2d_size(b2d_func)
        if not flags:
            driver.cuOccupancyMaxPotentialBlockSize(byref(gridsize), byref(blocksize),
                                                    func.handle,
                                                    b2d_cb,
                                                    memsize, blocksizelimit)
        else:
            driver.cuOccupancyMaxPotentialBlockSizeWithFlags(byref(gridsize), byref(blocksize),
                                                             func.handle, b2d_cb,
                                                             memsize, blocksizelimit, flags)
        return (gridsize.value, blocksize.value)

    此函數在中調用numba/cuda/compiler:

    def _compute_thread_per_block(self, kernel):
        tpb = self.thread_per_block
        # Prefer user-specified config
        if tpb != 0:
            return tpb
        # Else, ask the driver to give a good cofnig
        else:
            ctx = get_context()
            kwargs = dict(
                func=kernel._func.get(),
                b2d_func=lambda tpb: 0,
                memsize=self.sharedmem,
                blocksizelimit=1024,
            )
            try:
                # Raises from the driver if the feature is unavailable
                _, tpb = ctx.get_max_potential_block_size(**kwargs)
            except AttributeError:
                # Fallback to table-based approach.
                tpb = self._fallback_autotune_best(kernel)
                raise
            return tpb

    仔細查看的函數定義_compute_thread_per_block,您可以看到一個寫為 Python lambda 的回調:b2d_func=lambda tpb: 0.

    啊哈!在這個 CUDA 調用的中間,回調函數必須獲取 Python GIL 才能執行只返回 0 的函數。這是因為執行任何 Python 代碼都需要 GIL ,并且在任何給定的時間點只能由單個線程擁有

    用純 C 函數代替它就解決了這個問題。你可以用 Numba 從 Python 中編寫一個純 C 函數!

    @cfunc("uint64(int32)")
    def _b2d_func(tpb):
        return 0
     
    b2d_func=_b2d_func.address

    此修復程序已提交并最終合并到Numba PR #4581Numba 中的這五行代碼更改最終解決了幾個人在數周的調試中大量編寫代碼的問題。

    調試經驗教訓

    在各種調試過程中,甚至在錯誤最終解決后,我們都反思了這個問題,并得出了以下教訓:

    • 不要實現死鎖。真的,不要!
    • 不要將 Python 函數作為回調傳遞給 C / C ++函數,除非您絕對確定在執行回調時 GIL 不會被另一個線程占用。即使你絕對確定 GIL 沒有被拿走,也要進行雙重和三次檢查。你不想在這里冒險。
    • 使用您所掌握的所有工具。盡管您主要編寫 Python 代碼,但在用其他語言(如 C 或 C ++)編寫的庫中仍然可以發現錯誤。 GDB 在調試 C 和 C ++以及 Python 方面功能強大。有關詳細信息,請參閱GDB 支持.

    Bug 復雜性與代碼修復復雜性相比

    圖 2 中的圖表示了一種常見的調試模式:理解和發現問題所花費的時間很長,而更改的程度很低。這種情況就是這種模式的一個理想例子:調試時間趨于無窮大,編寫或更改的代碼行趨于零。

    Chart showing the length of change compared to debugging time invested.
    圖 2 :解決問題所需的代碼行與調試時間成反比

    結論

    調試可能會讓人望而生畏,尤其是當您無法訪問所有源代碼或一個好的 IDE 時。盡管 GDB 看起來很可怕,但它也同樣強大。然而,隨著時間的推移,有了正確的工具、經驗和知識,看似不可能理解的問題可以被不同程度的細節看待,并得到真正的理解

    這篇文章一步一步地概述了一個 bug 是如何花了一個多方面的開發團隊幾十個工程小時來解決的。有了這個概述和對 GDB 、多線程和死鎖的一些理解,您可以使用新獲得的技能來幫助解決中等復雜的問題。

    最后,永遠不要局限于你已經知道的工具。如果你知道 PDB ,接下來試試 GDB 。如果您對操作系統調用堆棧有足夠的了解,請嘗試探索寄存器和其他 CPU 屬性。這些技能當然可以幫助所有領域和編程語言的開發人員更加意識到潛在的陷阱,并提供獨特的機會來防止愚蠢的錯誤成為噩夢般的怪物。

    ?

    +1

    標簽

    人人超碰97caoporen国产