diff --git a/.translate/state/numba.md.yml b/.translate/state/numba.md.yml index 73a8e2c..fbbdfa3 100644 --- a/.translate/state/numba.md.yml +++ b/.translate/state/numba.md.yml @@ -1,6 +1,6 @@ -source-sha: a897eb875ecbf8975872fd6bc0a6b2e91a6ba701 +source-sha: be6eeaee8db0c8bfea65b89d57ca8aecf7f96dff synced-at: "2026-04-12" model: claude-sonnet-4-6 mode: UPDATE -section-count: 6 +section-count: 5 tool-version: 0.14.1 diff --git a/lectures/numba.md b/lectures/numba.md index b5b92e8..35c1fa7 100644 --- a/lectures/numba.md +++ b/lectures/numba.md @@ -13,13 +13,14 @@ translation: title: Numba headings: Overview: مروری کلی - Compiling Functions: کامپایل توابع + Compiling Functions: کامپایل کردن توابع Compiling Functions::An Example: یک مثال - Compiling Functions::How and When it Works: چگونه و چه زمانی کار می‌کند - Type Inference: استنتاج نوع - Dangers and Limitations: خطرات و محدودیت‌ها - Dangers and Limitations::Limitations: محدودیت‌ها - 'Dangers and Limitations::A Gotcha: Global Variables': 'یک مشکل: متغیرهای سراسری' + Compiling Functions::An Example::Base Version: نسخه پایه + Compiling Functions::An Example::Acceleration via Numba: شتاب‌دهی از طریق Numba + Compiling Functions::How and When it Works: چگونگی و شرایط عملکرد آن + Sharp Bits: نکات ظریف + Sharp Bits::Typing: تعیین نوع + Sharp Bits::Global Variables: متغیرهای سراسری Multithreaded Loops in Numba: حلقه‌های چندنخی در Numba Exercises: تمرین‌ها --- @@ -58,33 +59,32 @@ import matplotlib.pyplot as plt در یک {doc}`درس قبلی ` درباره برداری‌سازی بحث کردیم، که می‌تواند سرعت اجرا را با ارسال دسته‌ای عملیات پردازش آرایه به کد کارآمد سطح پایین بهبود بخشد. -با این حال، همانطور که {ref}`قبلاً بحث شد `، طرح‌های سنتی برداری‌سازی، مانند آنچه در MATLAB و NumPy یافت می‌شود، چندین نقطه ضعف دارند. +با این حال، همانطور که {ref}`قبلاً بحث شد `، طرح‌های سنتی برداری‌سازی چندین نقطه ضعف دارند: -* برای عملیات ترکیبی آرایه، بسیار حافظه‌بر هستند -* برای برخی الگوریتم‌ها، ناکارآمد یا غیرممکن است. +* بسیار حافظه‌بر برای عملیات ترکیبی آرایه +* ناکارآمد یا غیرممکن برای برخی الگوریتم‌ها یک راه برای دور زدن این مشکلات، استفاده از [Numba](https://numba.pydata.org/) است، یک -**کامپایلر در زمان اجرا (JIT)** برای Python که به سمت کار عددی جهت‌گیری شده است. +**کامپایلر در زمان اجرا (JIT)** برای Python. -Numba توابع را در حین اجرا به دستورالعمل‌های کد ماشین بومی کامپایل می‌کند. +Numba توابع را در زمان اجرا به دستورالعمل‌های کد ماشین بومی کامپایل می‌کند. وقتی موفق می‌شود، نتیجه عملکردی قابل مقایسه با C یا Fortran کامپایل‌شده است. -علاوه بر این، Numba می‌تواند ترفندهای مفید دیگری نیز انجام دهد، مانند {ref}`چندنخی ` یا -ارتباط با GPU‌ها (از طریق `numba.cuda`). - -کامپایلر JIT در Numba از بسیاری جهات مشابه کامپایلر JIT در Julia است. - -تفاوت اصلی این است که کمتر بلندپروازانه است و تلاش می‌کند زیرمجموعه کوچک‌تری از زبان را کامپایل کند. - -اگرچه این ممکن است به نظر یک نقص برسد، اما به نوعی یک مزیت است. - -Numba سبک، آسان برای استفاده، و در آنچه انجام می‌دهد بسیار خوب است. +علاوه بر این، Numba می‌تواند ترفندهای مفیدی مانند {ref}`چندنخی ` نیز انجام دهد. این درس ایده‌های اصلی را معرفی می‌کند. +```{note} +برخی خوانندگان ممکن است کنجکاو رابطه بین Numba و [Julia](https://julialang.org/) باشند، +که کامپایلر JIT خود را دارد. در حالی که این دو کامپایلر از بسیاری جهات مشابه هستند، +Numba کمتر بلندپروازانه است و تنها تلاش می‌کند زیرمجموعه کوچکی از +زبان Python را کامپایل کند. هرچند این ممکن است یک نقص به نظر برسد، اما یک مزیت نیز هست: +ماهیت محدودتر Numba استفاده از آن را آسان و در آنچه انجام می‌دهد کارآمد می‌کند. +``` + (numba_link)= -## {index}`کامپایل توابع ` +## {index}`کامپایل کردن توابع ` ```{index} single: Python; Numba ``` @@ -101,16 +101,14 @@ $$ x_{t+1} = \alpha x_t (1 - x_t) $$ -در ادامه تنظیم می‌کنیم: +در ادامه، $\alpha = 4$ تعیین می‌کنیم. -```{code-cell} ipython3 -α = 4.0 -``` +#### نسخه پایه -در اینجا نمودار یک مسیر نمونه‌وار، با شروع از $x_0 = 0.1$، با $t$ بر روی محور x آمده است: +در اینجا نمودار یک مسیر معمولی نشان داده شده است، که از $x_0 = 0.1$ شروع می‌شود، با $t$ روی محور افقی: ```{code-cell} ipython3 -def qm(x0, n): +def qm(x0, n, α=4.0): x = np.empty(n+1) x[0] = x0 for t in range(n): @@ -125,97 +123,112 @@ ax.set_ylabel('$x_{t}$', fontsize = 12) plt.show() ``` -برای افزایش سرعت تابع `qm` با استفاده از Numba، اولین گام ما این است: +بیایید ببینیم برای $n$ بزرگ، اجرای این کد چقدر زمان می‌برد: ```{code-cell} ipython3 -from numba import jit +n = 10_000_000 + +with qe.Timer() as timer1: + # Time Python base version + x = qm(0.1, int(n)) -qm_numba = jit(qm) ``` -تابع `qm_numba` نسخه‌ای از `qm` است که برای کامپایل JIT «هدف‌گذاری» شده است. +#### شتاب‌دهی از طریق Numba -لحظه‌ای بعد توضیح خواهیم داد که این به چه معناست. +برای تسریع تابع `qm` با استفاده از Numba، ابتدا تابع `jit` را وارد می‌کنیم: -بیایید فراخوانی‌های تابع یکسان را در این دو نسخه زمان‌بندی و مقایسه کنیم، با شروع از تابع اصلی `qm`: ```{code-cell} ipython3 -n = 10_000_000 +from numba import jit +``` -with qe.Timer() as timer1: - qm(0.1, int(n)) -time1 = timer1.elapsed +اکنون آن را روی `qm` اعمال می‌کنیم تا تابع جدیدی تولید شود: + +```{code-cell} ipython3 +qm_numba = jit(qm) ``` -حالا بیایید `qm_numba` را امتحان کنیم: +تابع `qm_numba` نسخه‌ای از `qm` است که برای کامپایل JIT «هدف‌گذاری» شده است. + +معنای این موضوع را به زودی توضیح خواهیم داد. + +بیایید زمان این نسخه جدید را اندازه بگیریم: ```{code-cell} ipython3 with qe.Timer() as timer2: - qm_numba(0.1, int(n)) -time2 = timer2.elapsed + # Time jitted version + x = qm_numba(0.1, int(n)) ``` -این از قبل یک افزایش سرعت بسیار زیاد است. +این یک افزایش سرعت قابل توجه است. -در واقع، دفعه بعد و تمام دفعات بعدی حتی سریع‌تر اجرا می‌شود، چرا که تابع کامپایل شده و در حافظه قرار دارد: +در واقع، در دفعه بعد و تمام اجراهای بعدی، حتی سریع‌تر اجرا می‌شود؛ زیرا تابع کامپایل شده و در حافظه ذخیره است: (qm_numba_result)= ```{code-cell} ipython3 with qe.Timer() as timer3: - qm_numba(0.1, int(n)) -time3 = timer3.elapsed + # Second run + x = qm_numba(0.1, int(n)) ``` +در اینجا میزان افزایش سرعت نشان داده شده است: + ```{code-cell} ipython3 -time1 / time3 # Calculate speed gain +timer1.elapsed / timer3.elapsed ``` -### چگونه و چه زمانی کار می‌کند +این یک بهبود چشمگیر با تغییری اندک در کد اصلی ماست. + +بیایید بررسی کنیم که این چگونه کار می‌کند. -Numba تلاش می‌کند با استفاده از زیرساخت ارائه‌شده توسط [پروژه LLVM](https://llvm.org/) کد ماشین سریع تولید کند. +### چگونگی و شرایط عملکرد آن -این کار را از طریق استنتاج اطلاعات نوع به صورت بلادرنگ انجام می‌دهد. +Numba تلاش می‌کند با استفاده از زیرساخت ارائه‌شده توسط [پروژه LLVM](https://llvm.org/)، کد ماشین سریع تولید کند. -(برای بحث درباره نوع‌ها، {doc}`درس ` قبلی ما در مورد محاسبات علمی را ببینید.) +این کار را از طریق استنتاج اطلاعات نوع به صورت پویا انجام می‌دهد. -ایده اساسی این است: +(برای بحث درباره انواع داده، به {doc}`درس قبلی ` ما در مورد محاسبات علمی مراجعه کنید.) -* پایتون بسیار انعطاف‌پذیر است و از این رو می‌توانیم تابع `qm` را با انواع مختلفی فراخوانی کنیم. - * به عنوان مثال، `x0` می‌تواند یک آرایه NumPy یا یک لیست باشد، `n` می‌تواند یک عدد صحیح یا یک عدد اعشاری باشد و غیره. -* این امر تولید کد ماشین کارآمد *از پیش* (یعنی قبل از زمان اجرا) را بسیار دشوار می‌کند. -* اما وقتی در واقع تابع را *فراخوانی* می‌کنیم، مثلاً با اجرای `qm(0.5, 10)`، - نوع‌های `x0` و `n` مشخص می‌شوند. -* علاوه بر این، نوع‌های *سایر متغیرها* در `qm` *پس از مشخص شدن نوع‌های ورودی قابل استنتاج هستند*. -* بنابراین استراتژی Numba و سایر کامپایلرهای JIT این است که *تا زمان فراخوانی تابع صبر کنند*، و سپس کامپایل کنند. +ایده اصلی به این شرح است: -این روش «کامپایل در لحظه» نام دارد. +* پایتون بسیار انعطاف‌پذیر است و از این رو می‌توان تابع qm را با انواع داده مختلفی فراخواند. + * برای مثال، `x0` می‌تواند یک آرایه NumPy یا یک لیست باشد، `n` می‌تواند عدد صحیح یا اعشاری باشد و غیره. +* این موضوع تولید کد ماشین کارآمد *از پیش* (یعنی قبل از زمان اجرا) را بسیار دشوار می‌سازد. +* اما هنگامی که تابع را واقعاً *فراخوانی* می‌کنیم، مثلاً با اجرای `qm(0.5, 10)`، انواع `x0`، `α` و `n` مشخص می‌شوند. +* علاوه بر این، انواع *سایر متغیرها* در `qm` *پس از مشخص شدن انواع ورودی قابل استنتاج هستند*. +* پس راهبرد Numba و سایر کامپایلرهای JIT این است که *تا زمان فراخوانی تابع صبر کنند* و سپس آن را کامپایل کنند. -توجه داشته باشید که اگر `qm(0.5, 10)` را فراخوانی کنید و سپس `qm(0.9, 20)` را دنبال آن بیاورید، کامپایل فقط در فراخوانی اول انجام می‌شود. +این روش «کامپایل درست به موقع» (just-in-time) نامیده می‌شود. -این به این دلیل است که کد کامپایل‌شده در حافظه نهان ذخیره و در صورت نیاز مجدداً استفاده می‌شود. +توجه داشته باشید که اگر `qm_numba(0.5, 10)` را فراخوانی کنید و سپس `qm_numba(0.9, 20)` را اجرا کنید، کامپایل فقط در اولین فراخوانی انجام می‌شود. -به همین دلیل است که در کد بالا، `time3` از `time2` کوچک‌تر است. +این به این دلیل است که کد کامپایل‌شده در حافظه نهان ذخیره می‌شود و در صورت نیاز مجدداً استفاده می‌شود. + +به همین دلیل است که در کد بالا، اجرای دوم `qm_numba` سریع‌تر است. ```{admonition} توضیح -در عمل، به جای نوشتن `qm_numba = jit(qm)`، از نحو *دکوراتور* استفاده می‌کنیم و `@jit` را قبل از تعریف تابع قرار می‌دهیم. این معادل اضافه کردن `qm = jit(qm)` بعد از تعریف است. ما از این نحو در بقیه درس استفاده می‌کنیم. (برای اطلاعات بیشتر در مورد دکوراتورها، {doc}`python_advanced_features` را ببینید.) +در عمل، به جای نوشتن `qm_numba = jit(qm)`، معمولاً از نحو +*دکوراتور* استفاده می‌کنیم و `@jit` را قبل از تعریف تابع قرار می‌دهیم. این +معادل افزودن `qm = jit(qm)` پس از تعریف است. ``` -## استنتاج نوع +## نکات ظریف -استنتاج موفقیت‌آمیز نوع، بخش کلیدی کامپایل JIT است. +استفاده از Numba نسبتاً آسان است، اما همیشه بی‌دردسر نیست. -همانطور که می‌توانید تصور کنید، استنتاج انواع برای اشیاء ساده Python (مثلاً انواع داده اسکالر ساده مانند اعشاری و صحیح) آسان‌تر است. +بیایید برخی از مشکلاتی که کاربران با آن‌ها مواجه می‌شوند را مرور کنیم. -Numba همچنین با آرایه‌های NumPy که انواع به خوبی تعریف شده دارند، به خوبی کار می‌کند. +### تعیین نوع -در یک محیط ایده‌آل، Numba می‌تواند تمام اطلاعات نوع لازم را استنتاج کند. +استنتاج موفق نوع، کلید کامپایل JIT است. -این به آن اجازه می‌دهد تا کد ماشین بومی تولید کند، بدون نیاز به فراخوانی محیط زمان اجرای Python. +در یک محیط ایده‌آل، Numba می‌تواند تمام اطلاعات نوع لازم را استنتاج کند. -وقتی Numba نمی‌تواند تمام اطلاعات نوع را استنتاج کند، خطا ایجاد می‌کند. +زمانی که Numba *نتواند* تمام اطلاعات نوع را استنتاج کند، خطا صادر می‌کند. -به عنوان مثال، در تنظیم زیر، Numba قادر به تعیین نوع تابع `g` هنگام کامپایل `iterate` نیست +برای مثال، در حالت زیر، Numba قادر به تعیین نوع تابع `g` هنگام کامپایل `iterate` نیست. ```{code-cell} ipython3 @jit @@ -229,14 +242,14 @@ def iterate(f, x0, n): def g(x): return np.cos(x) - 2 * np.sin(x) -# این کد خطا ایجاد می‌کند +# This code throws an error try: iterate(g, 0.5, 100) except Exception as e: print(e) ``` -می‌توانیم این خطا را به راحتی با کامپایل کردن `g` برطرف کنیم. +در این حالت، می‌توانیم این مشکل را به‌راحتی با کامپایل کردن `g` برطرف کنیم. ```{code-cell} ipython3 @jit @@ -246,25 +259,13 @@ def g(x): iterate(g, 0.5, 100) ``` -## خطرات و محدودیت‌ها - -بیایید برخی یادداشت‌های احتیاطی اضافه کنیم. - -### محدودیت‌ها - -همانطور که دیدیم، Numba باید اطلاعات نوع را روی تمام متغیرها استنتاج کند تا دستورالعمل‌های سریع سطح ماشین تولید کند. - -برای روال‌های بزرگ‌تر، یا برای روال‌هایی که از کتابخانه‌های خارجی استفاده می‌کنند، این فرایند به راحتی می‌تواند شکست بخورد. +در موارد دیگر، مانند زمانی که می‌خواهیم از توابع کتابخانه‌های خارجی مانند `SciPy` استفاده کنیم، ممکن است هیچ راه‌حل آسانی وجود نداشته باشد. -از این رو، بهتر است روی تسریع قطعات کوچک و حیاتی کد تمرکز کنید. +### متغیرهای سراسری -این به شما عملکرد بسیار بهتری نسبت به پوشاندن برنامه‌های Python خود با عبارت‌های `@jit` می‌دهد. +نکته دیگری که هنگام استفاده از Numba باید به آن توجه کرد، نحوه مدیریت متغیرهای سراسری است. -### یک مشکل: متغیرهای سراسری - -چیز دیگری که هنگام استفاده از Numba باید مراقب آن باشید در اینجا است. - -مثال زیر را در نظر بگیرید +برای مثال، کد زیر را در نظر بگیرید. ```{code-cell} ipython3 a = 1 @@ -282,11 +283,12 @@ a = 2 print(add_a(10)) ``` -توجه داشته باشید که تغییر global هیچ تأثیری بر مقدار برگشتی توسط تابع نداشت. +توجه کنید که تغییر متغیر سراسری هیچ تأثیری بر مقدار بازگردانده‌شده توسط تابع نداشت 😱. + +هنگامی که Numba کد ماشین را برای توابع کامپایل می‌کند، متغیرهای سراسری را به‌عنوان ثابت در نظر می‌گیرد تا پایداری نوع را تضمین کند. -وقتی Numba کد ماشین را برای توابع کامپایل می‌کند، متغیرهای سراسری را به عنوان ثابت برای اطمینان از پایداری نوع درمان می‌کند. +برای جلوگیری از این مشکل، به‌جای تکیه بر متغیرهای سراسری، مقادیر را به‌عنوان آرگومان‌های تابع ارسال کنید. -برای جلوگیری از این مشکل، مقادیر را به عنوان آرگومان تابع ارسال کنید و به متغیرهای سراسری متکی نباشید. (multithreading)= ## حلقه‌های چندنخی در Numba @@ -315,16 +317,12 @@ $$ ```{code-cell} ipython3 @jit -def h(w, r=0.1, s=0.3, v1=0.1, v2=1.0): - """ - ثروت خانوار را به‌روزرسانی می‌کند. - """ - - # شوک‌ها را بکش +def update(w, r=0.1, s=0.3, v1=0.1, v2=1.0): + " Updates household wealth. " + # Draw shocks R = np.exp(v1 * np.random.randn()) * (1 + r) y = np.exp(v2 * np.random.randn()) - - # ثروت را به‌روزرسانی کن + # Update wealth w = R * s * w + y return w ``` @@ -338,7 +336,7 @@ T = 100 w = np.empty(T) w[0] = 5 for t in range(T-1): - w[t+1] = h(w[t]) + w[t+1] = update(w[t]) ax.plot(w) ax.set_xlabel('$t$', fontsize=12) @@ -350,22 +348,24 @@ plt.show() حل این موضوع با مداد و کاغذ آسان نیست، بنابراین به جای آن از شبیه‌سازی استفاده خواهیم کرد: -1. تعداد زیادی از خانوارها را در طول زمان شبیه‌سازی کنیم -2. میانه ثروت را محاسبه کنیم +1. تعداد زیادی از خانوارها را در طول زمان شبیه‌سازی می‌کنیم +2. میانه ثروت را محاسبه می‌کنیم در اینجا کد است: ```{code-cell} ipython3 @jit def compute_long_run_median(w0=1, T=1000, num_reps=50_000): - obs = np.empty(num_reps) + # For each household for i in range(num_reps): + # Set the initial condition and run forward in time w = w0 for t in range(T): - w = h(w) + w = update(w) + # Record the final value obs[i] = w - + # Take the median of all final values return np.median(obs) ``` @@ -373,6 +373,13 @@ def compute_long_run_median(w0=1, T=1000, num_reps=50_000): ```{code-cell} ipython3 with qe.Timer(): + # Warm up + compute_long_run_median() +``` + +```{code-cell} ipython3 +with qe.Timer(): + # Second run compute_long_run_median() ``` @@ -384,15 +391,15 @@ with qe.Timer(): from numba import prange @jit(parallel=True) -def compute_long_run_median_parallel(w0=1, T=1000, num_reps=50_000): - +def compute_long_run_median_parallel( + w0=1, T=1000, num_reps=50_000 + ): obs = np.empty(num_reps) - for i in prange(num_reps): + for i in prange(num_reps): # Parallelize over households w = w0 for t in range(T): - w = h(w) + w = update(w) obs[i] = w - return np.median(obs) ``` @@ -400,12 +407,19 @@ def compute_long_run_median_parallel(w0=1, T=1000, num_reps=50_000): ```{code-cell} ipython3 with qe.Timer(): + # Warm up + compute_long_run_median_parallel() +``` + +```{code-cell} ipython3 +with qe.Timer(): + # Second run compute_long_run_median_parallel() ``` افزایش سرعت قابل توجه است. -توجه کنید که موازی‌سازی را در سطح خانوارها انجام می‌دهیم نه در طول زمان — به‌روزرسانی‌های یک خانوار در طول دوره‌های زمانی ذاتاً ترتیبی هستند. +توجه داشته باشید که موازی‌سازی را در سطح خانوارها انجام می‌دهیم نه در طول زمان -- به‌روزرسانی‌های یک خانوار منفرد در طول دوره‌های زمانی ذاتاً ترتیبی هستند. برای موازی‌سازی مبتنی بر GPU، به {doc}`درس‌های ما درباره JAX ` مراجعه کنید.