惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
S
SegmentFault 最新的问题
量子位
A
Arctic Wolf
L
Lohrmann on Cybersecurity
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
WordPress大学
WordPress大学
V
Vulnerabilities – Threatpost
博客园 - Franky
C
Cyber Attacks, Cyber Crime and Cyber Security
The Cloudflare Blog
Last Week in AI
Last Week in AI
The Hacker News
The Hacker News
I
Intezer
J
Java Code Geeks
P
Privacy International News Feed
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
S
Secure Thoughts
Cisco Talos Blog
Cisco Talos Blog
阮一峰的网络日志
阮一峰的网络日志
S
Securelist
Security Latest
Security Latest
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
小众软件
小众软件
Jina AI
Jina AI
有赞技术团队
有赞技术团队
人人都是产品经理
人人都是产品经理
博客园_首页
酷 壳 – CoolShell
酷 壳 – CoolShell
T
The Exploit Database - CXSecurity.com
雷峰网
雷峰网
T
Tenable Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
P
Privacy & Cybersecurity Law Blog
Simon Willison's Weblog
Simon Willison's Weblog
博客园 - 【当耐特】
T
Threat Research - Cisco Blogs
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
MongoDB | Blog
MongoDB | Blog
D
DataBreaches.Net
N
News | PayPal Newsroom
Google Online Security Blog
Google Online Security Blog
K
Kaspersky official blog
H
Help Net Security
宝玉的分享
宝玉的分享
罗磊的独立博客
Webroot Blog
Webroot Blog
月光博客
月光博客
B
Blog RSS Feed
Recorded Future
Recorded Future

hsfzxjy 的博客

解决 VSCode + CMake + MSVC 编译器信息乱码的问题 使用 3090 部署 1.58bit 动态量化版 DeepSeek R1 671b 如何在 VS Code DevContainer 中配置 HTTP 代理 如何在跳板机背后的服务器上使用 VS Code Remote - Containers Cohesive Digests for Ints and Floats Rust 中的隐匿概念 —— Place(位置) 美术馆 一尺之槌,日取其半,1075日而竭 老生常谈:使用 Cloudflare 自选 IP 加速站点访问 辩义 State、Nation 与 Country 将 Base64 编码的数据快速转换为 Uint8Array 折腾 NPU·第1章 —— 搭建 Level Zero 开发环境 折腾 NPU·第0章 —— Intel NPU 概述与 Level-Zero 新增域名 monad.run CSS 中为特定字符设置不同字体 Arbitary Lifetime Transmutation via Rust Unsoundness Dijkstra 算法的延伸 Manacher 回文计数算法 硬卧 Go Fact: Zero-sized Field at the Rear of a Struct Has Non-zero Size Display *big.Rat Losslessly and Smartly in Golang 代码的仪式 Building Electron From Scratch 中式亲属称谓研究之一:构建半群 Some Notes on Kotlin Coroutines Git sparse-checkout and partial clones for Mega-Repos 辩义“封建” Diving from the CUDA Error 804 into a bug of libnvidia-container Modern Cryptography, GPG and Integration with Git(hub) Move the Root Partition of Ubuntu A New Programmer Kicks a Roadblock Git-based Dependencies in Dart and Go Reversy Naming 人类一败涂地 Invalid Golang Pointers Can Bite You Even If You Don't Dereference Side Project(副业) A Flaw of Promoting Complex Trait Bounds in Rust Initialize Process Pool Worker with Individual Value Rust - Python FFI From Scratch [Extending Hexo For My Site] Part 1 [Extending Hexo For My Site] Part 0 Debug a 'torch.tensor(1).cuda()' hanging 不自由的互联网 Retrieve Contents over HTTP without curl or wget [Unravelling mocona] Part 1 - Verbosity or Anti-Pattern [Unravelling mocona] Part 0 - Preface Understanding pickle in Python Rough Notes on Deploying Vaultwarden & NextCloud Bookmarks 语言狂热者与实用主义者 Demystify the randomness in CUDA kernels Performant Bulk Mutations in IndexedDB Auto Rebuild .pyx Files with pyximport Obtain a Random Available TCP Port with Bash Information Theory: KL Divergence Information Theory: Entropy and Mutual Information 铁板烧 西郊线 Proof of the Gumbel Max Trick Option::as_ref Rc, RefCell and Interior Mutability Visualizing Correlation 三月十日杂感 三月一日杂感 二月十一日杂感 一月二十六日杂感 SS Configuration 一月七日杂感 四月·病 Haskell 笔记:State Monad Haskell 笔记:Monad 引论 Haskell 笔记:Applicative Haskell 笔记:Category Theory and Functor Haskell 笔记:data, type, newtype Haskell 笔记:folds 使用 Aria2 在 Ubuntu 中下载百度云资源 从伪并行的 Python 多线程说起 一个 Reentrant Error 引发的对 Python 信号机制的探索和思考 Linux 文件权限 HSFZMUN 4.0 部署小记 午后雨·科大 最后的雨夜·广州 揭秘·变态的平方根倒数算法 神坑·Python 装饰类无限递归 Python“黑魔法”之 Encoding & Decoding Ubuntu 重新映射键盘布局 为什么我要翻墙 Python“黑魔法”之 Generator Coroutines 数学美 之 判断线段相交的最简方法 除夕杂感 17 行代码实现的简易 Javascript 字符串模板 Python“黑魔法”之 Meta Classes 诗集 生活,需要被“发现” 家书·十八岁成人礼 炫技?还是需求? 【译】响应式图片的现状 【译】“为什么有这么多的编程语言?” Wisecity 商赛总结——也谈前端自动化测试 记一次 DoS 诈骗网站的经历 那一年,我们望向星空
Cython and Threads
2021-05-07 · via hsfzxjy 的博客

Pure Python sucks in the scene of parallel computing, due to the existence of the Global Interpreter Lock (aka GIL). GIL prevents accessing or manipulating interpreter from different threads concurrently. The mechanism alleviates the risk of race condition, but sequentializes multi-threading program 1 as well. Sadly, there’s no way to release the lock from pure Python.

Alright. So what about beyond pure Python? Shall we bypass the mechanism within an extension? The answer is yes, and that’s what most of scientific computing libaries do.

Cython is a good choice for writing extensions, less verbose, and more similar to Python syntactically. In Cython, one can release GIL temporarily for a code block using the with nogil: syntax. Will it release the true power of multi-core CPU? We should have a try.

We adopt a toy example, say, a naive matrix multiplication, for benchmarking. Start with a C-only version:







import numpy as np
cimport numpy as np

cdef void _matmul(
np.float_t[:, :] Av,
np.float_t[:, :] Bv,
np.float_t[:, :] Cv,
int M, int N, int P,
) nogil:
cdef int i, j, k

for i in range(M):
for j in range(P):
for k in range(N):
Cv[i, j] += Av[i, k] * Bv[k, j]

The function above is straight-forward. We then create a wrapper for it, so that it can be called by Python code:

cpdef matmul(
np.ndarray[dtype=np.float_t, ndim=2] A,
np.ndarray[dtype=np.float_t, ndim=2] B,
object use_gil,
):
cdef int M = A.shape[0]
cdef int N = A.shape[1]
cdef int P = B.shape[1]

C = np.zeros((M, P))
cdef np.float_t[:, :] Av = A, Bv = B, Cv = C

if use_gil:
_matmul(Av, Bv, Cv, M, N, P)
else:
with nogil:
_matmul(Av, Bv, Cv, M, N, P)
return C

Now the Cython part is ready. Below a script for benchmarking:

import timeit
import threading
import itertools

import pyximport
import numpy as np

pyximport.install(setup_args={"include_dirs": np.get_include()}, inplace=True, language_level=3)
import matmul

N = 1200
A = np.random.rand(N, N)
B = np.random.rand(N, N)

def runner(nthreads: int, use_gil: bool) -> None:
args = (A, B, use_gil)

threads = []
for _ in range(nthreads):
threads.append(threading.Thread(target=matmul.matmul, args=args))
threads[-1].start()

for thread in threads:
thread.join()

def make_grid(**kwargs):
space = [[(name, v) for v in lst] for name, lst in kwargs.items()]
return map(dict, itertools.product(*space))

for kw in make_grid(
nthreads=[1, 2],
use_gil=[True, False],
):
print(kw)
print(timeit.timeit("runner(**kw)", globals=dict(runner=runner, kw=kw), number=1))

Two matrices with a rather large size 1200 x 1200 are supplied as input, and we test matmul against four settings. The result is listed as below:

nthreadsGILtime (s)
1N3.47
1Y3.51
2N3.78
2Y6.96

The first two rows show that, with single thread, matmul has comparable performance no matter releasing GIL or not. This is desired behavior, since GIL should not lead to performance degradation in single-threading scene. But things change when it comes to multi-threading. With two computing threads running in parallel, the time doubles if holding GIL, whilst in another setting (GIL released), the performance remains unchanged.

We may step further to investigate the behavior of prange. prange is provided by Cython for more convenient parallel computing, adopting the famous OpenMP as backend. Writing a prange version _matmul should take minor modification:

cdef void _matmul_p(
np.float_t[:, :] Av,
np.float_t[:, :] Bv,
np.float_t[:, :] Cv,
int M, int N, int P,
) nogil:
cdef int i, j, k, ij

cdef int MP = M * P
for ij in prange(MP, schedule='guided'):
i = ij // P
j = ij % P
for k in range(N):
Cv[i, j] += Av[i, k] * Bv[k, j]

plus the wrapper matmul:

cpdef matmul(
np.ndarray[dtype=np.float_t, ndim=2] A,
np.ndarray[dtype=np.float_t, ndim=2] B,
object use_gil,

object parallel,

):
cdef int M = A.shape[0]
cdef int N = A.shape[1]
cdef int P = B.shape[1]

C = np.zeros((M, P))
cdef np.float_t[:, :] Av = A, Bv = B, Cv = C

if use_gil:

if parallel:
_matmul_p(Av, Bv, Cv, M, N, P)
else:
_matmul(Av, Bv, Cv, M, N, P)

else:

if parallel:
_matmul_p(Av, Bv, Cv, M, N, P)
else:
with nogil:
_matmul(Av, Bv, Cv, M, N, P)

return C

and also, the benchmark script:


def runner(nthreads: int, use_gil: bool, parallel: bool) -> None:
args = (A, B, use_gil, parallel)

if nthreads == 0:
matmul.matmul(*args)
return

threads = []
for _ in range(nthreads):
threads.append(threading.Thread(target=matmul.matmul, args=args))
threads[-1].start()

for thread in threads:
thread.join()
for kw in make_grid(
nthreads=[0, 1, 2],
use_gil=[True, False],

parallel=[True, False],

):
print(kw)
print(timeit.timeit("runner(**kw)", globals=dict(runner=runner, kw=kw), number=1))

OpenMP requires extra compilation flags, so a .pyxbld file is needed:


from setuptools import Extension
from Cython.Build import cythonize


def make_ext(modname, pyxfilename):
ext = Extension(
modname,
[pyxfilename],
extra_compile_args=['-fopenmp'],
extra_link_args=['-fopenmp'],
)
return cythonize(
[ext],
language_level=3,
annotate=True,
)[0]
nthreadsGILtime w/o par. (s)time w/ par. (s)
1N3.470.82
1Y3.510.89
2N3.781.79
2Y6.961.81

We can see that prange brings an amazing boost in performance! _matmul_p is 3~4x faster in single-threading setting. The number might vary across different hardwares, depending on the number of CPU cores. In the setting of two threads, the running time doubles, which indicates that prange does efficiently use up all available CPU resources.

We can also notice that, whether to release GIL or not seemingly does not affect prange 2. The reason is prange requires GIL to be released, which is automatically done by default.

Cython supports native parallelism through the cython.parallel module. To use this kind of parallelism, the GIL must be released (see Releasing the GIL). It currently supports OpenMP, but later on more backends might be supported. – Using Parallelism

Conclusion

If there’s no need to hold GIL, just release it. This happens when you are manipulating some C data structures, and not attempting to disturb the interpreter.

If there’s massive looping in your Cython code, feel free to accelerate it with prange. prange will effeciently schedule the computation onto all CPU resources.

If there’s some macro tasks 3 which could not be easily parallelized in Cython, schedule them via threading module. threading sucks for most of the time, but if the tasks not always acquiring GIL, it should be fine just like threads in other languages.


Author: hsfzxjy.
Link: .
License: CC BY-NC-ND 4.0.
All rights reserved by the author.
Commercial use of this post in any form is NOT permitted.
Non-commercial use of this post should be attributed with this block of text.