Skip to content

Commit 3748a7e

Browse files
committed
Update translation: lectures/numba.md
1 parent 77708bd commit 3748a7e

1 file changed

Lines changed: 272 additions & 21 deletions

File tree

lectures/numba.md

Lines changed: 272 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,17 @@ with qe.Timer():
425425

426426
## تمرین‌ها
427427

428+
{ref}`speed_ex1` و {ref}`numba_ex3` هر دو $\pi$ را با Monte Carlo از نمونه‌های تصادفی در مربع واحد تخمین می‌زنند.
429+
430+
ما آن‌ها را اینجا تولید می‌کنیم و در `u_draws` و `v_draws` ذخیره می‌کنیم تا بتوانیم در هر دو تمرین از آن‌ها استفاده کرده و نتایج را مقایسه کنیم.
431+
432+
```{code-cell} ipython3
433+
n = 1_000_000
434+
rng = np.random.default_rng()
435+
u_draws = rng.uniform(size=n)
436+
v_draws = rng.uniform(size=n)
437+
```
438+
428439
```{exercise}
429440
:label: speed_ex1
430441
@@ -443,10 +454,11 @@ with qe.Timer():
443454

444455
```{code-cell} ipython3
445456
@jit
446-
def calculate_pi(n=1_000_000):
457+
def calculate_pi(u_draws, v_draws):
458+
n = len(u_draws)
447459
count = 0
448460
for i in range(n):
449-
u, v = np.random.uniform(0, 1), np.random.uniform(0, 1)
461+
u, v = u_draws[i], v_draws[i]
450462
d = np.sqrt((u - 0.5)**2 + (v - 0.5)**2)
451463
if d < 0.5:
452464
count += 1
@@ -459,17 +471,66 @@ def calculate_pi(n=1_000_000):
459471

460472
```{code-cell} ipython3
461473
with qe.Timer():
462-
calculate_pi()
474+
calculate_pi(u_draws, v_draws)
463475
```
464476

465477
```{code-cell} ipython3
466478
with qe.Timer():
467-
calculate_pi()
479+
calculate_pi(u_draws, v_draws)
468480
```
469481

470-
اگر کامپایل JIT را با حذف `@jit` خاموش کنیم، کد حدود 150 برابر بیشتر در دستگاه ما طول می‌کشد.
482+
اگر کامپایل JIT را با حذف `@jit` خاموش کنیم، کد به طور قابل توجهی بیشتر در دستگاه ما طول می‌کشد.
483+
484+
بنابراین با افزودن چهار کاراکتر، افزایش سرعت بزرگی به دست می‌آوریم.
485+
486+
راه‌حل بالا یکی از دو رویکرد طبیعی را در پیش می‌گیرد: ابتدا *همه نقاط تصادفی را می‌کشد*، آن‌ها را در `u_draws` و `v_draws` ذخیره می‌کند و سپس اجازه می‌دهد تابع jit شده روی آن‌ها حلقه بزند.
487+
488+
رویکرد دیگر *کشیدن هر نقطه درون حلقه* است.
489+
490+
برای انجام این کار با یک `Generator` NumPy، ما `rng` را به عنوان آرگومان عبور می‌دهیم و `rng.uniform()` را درون بدنه حلقه فراخوانی می‌کنیم
491+
492+
```{code-cell} ipython3
493+
@jit
494+
def calculate_pi_in_loop(rng, n):
495+
count = 0
496+
for i in range(n):
497+
u, v = rng.uniform(), rng.uniform()
498+
d = np.sqrt((u - 0.5)**2 + (v - 0.5)**2)
499+
if d < 0.5:
500+
count += 1
501+
return (count / n) * 4
502+
```
503+
504+
```{code-cell} ipython3
505+
with qe.Timer():
506+
calculate_pi_in_loop(rng, n)
507+
```
508+
509+
```{code-cell} ipython3
510+
with qe.Timer():
511+
calculate_pi_in_loop(rng, n)
512+
```
513+
514+
دو سلولی که رویکرد اول را زمان‌بندی می‌کنند فقط حلقه را اندازه‌گیری می‌کنند --- نقاط تصادفی آن یک‌بار در بلوک راه‌اندازی مشترک بالا کشیده شده‌اند و هرگز زمان‌بندی نمی‌شوند، در حالی که رویکرد دوم هزینه کشیدن نقاط را درون تابع زمان‌بندی‌شده می‌پردازد.
515+
516+
برای مقایسه منصفانه این دو رویکرد، ما رویکرد اول را از ابتدا تا انتها زمان‌بندی می‌کنیم، از جمله هزینه تولید آرایه‌ها:
517+
518+
```{code-cell} ipython3
519+
with qe.Timer():
520+
u2 = rng.uniform(size=n)
521+
v2 = rng.uniform(size=n)
522+
calculate_pi(u2, v2)
523+
```
524+
525+
در این تنظیم سریال، دو رویکرد تخمین‌های به همان اندازه خوب می‌دهند و با سرعت مشابه اجرا می‌شوند، اما در *مصرف حافظه* معادل نیستند.
526+
527+
رویکرد اول باید همه $2n$ کشیدن را یک‌جا در حافظه نگه دارد --- دو آرایه از `n` عدد اعشاری، یا حدود `16n` بایت (حدود $1.6$ گیگابایت وقتی `n = 100_000_000`).
528+
529+
رویکرد دوم هر نقطه را در لحظه می‌کشد و آن را دور می‌ریزد، بنابراین ردپای حافظه‌اش با `n` رشد نمی‌کند.
471530

472-
بنابراین با افزودن چهار کاراکتر، افزایش سرعت 2 مرتبه بزرگی به دست می‌آوریم.
531+
این ممکن است پیشنهاد کند که کشیدن درون حلقه پیش‌فرض بهتری است.
532+
533+
اما همان‌طور که در {ref}`numba_ex_race` خواهیم دید، کشیدن درون حلقه با موازی‌سازی به بدی تعامل دارد.
473534

474535
```{solution-end}
475536
```
@@ -535,10 +596,13 @@ p, q = 0.1, 0.2 # احتمال خروج از حالت پایین و بالا ب
535596
در اینجا نسخه Python خالص تابع است
536597

537598
```{code-cell} ipython3
538-
def compute_series(n):
599+
n = 1_000_000
600+
rng = np.random.default_rng()
601+
U = rng.uniform(0, 1, size=n)
602+
603+
def compute_series(n, U):
539604
x = np.empty(n, dtype=np.int64)
540605
x[0] = 1 # در حالت 1 شروع کن
541-
U = np.random.uniform(0, 1, size=n)
542606
for t in range(1, n):
543607
current_x = x[t-1]
544608
if current_x == 0:
@@ -551,8 +615,7 @@ def compute_series(n):
551615
بیایید این کد را اجرا کنیم و بررسی کنیم که کسری از زمان صرف شده در حالت پایین حدود 0.666 است
552616

553617
```{code-cell} ipython3
554-
n = 1_000_000
555-
x = compute_series(n)
618+
x = compute_series(n, U)
556619
print(np.mean(x == 0)) # کسری از زمان که x در حالت 0 است
557620
```
558621

@@ -562,7 +625,7 @@ print(np.mean(x == 0)) # کسری از زمان که x در حالت 0 است
562625

563626
```{code-cell} ipython3
564627
with qe.Timer():
565-
compute_series(n)
628+
compute_series(n, U)
566629
```
567630

568631
بعد بیایید یک نسخه Numba پیاده‌سازی کنیم که آسان است
@@ -574,15 +637,15 @@ compute_series_numba = jit(compute_series)
574637
بیایید بررسی کنیم که هنوز اعداد صحیح دریافت می‌کنیم
575638

576639
```{code-cell} ipython3
577-
x = compute_series_numba(n)
640+
x = compute_series_numba(n, U)
578641
print(np.mean(x == 0))
579642
```
580643

581644
بیایید زمان را ببینیم
582645

583646
```{code-cell} ipython3
584647
with qe.Timer():
585-
compute_series_numba(n)
648+
compute_series_numba(n, U)
586649
```
587650

588651
این بهبود سرعت خوبی برای یک خط کد است!
@@ -616,10 +679,11 @@ with qe.Timer():
616679

617680
```{code-cell} ipython3
618681
@jit(parallel=True)
619-
def calculate_pi(n=1_000_000):
682+
def calculate_pi_parallel(u_draws, v_draws):
683+
n = len(u_draws)
620684
count = 0
621685
for i in prange(n):
622-
u, v = np.random.uniform(0, 1), np.random.uniform(0, 1)
686+
u, v = u_draws[i], v_draws[i]
623687
d = np.sqrt((u - 0.5)**2 + (v - 0.5)**2)
624688
if d < 0.5:
625689
count += 1
@@ -632,19 +696,196 @@ def calculate_pi(n=1_000_000):
632696

633697
```{code-cell} ipython3
634698
with qe.Timer():
635-
calculate_pi()
699+
calculate_pi_parallel(u_draws, v_draws)
636700
```
637701

638702
```{code-cell} ipython3
639703
with qe.Timer():
640-
calculate_pi()
704+
calculate_pi_parallel(u_draws, v_draws)
641705
```
642706

643707
با روشن و خاموش کردن موازی‌سازی (انتخاب `True` یا `False` در annotation `@jit`)، می‌توانیم افزایش سرعتی که چندنخی علاوه بر کامپایل JIT فراهم می‌کند را آزمایش کنیم.
644708

645-
در ایستگاه کاری ما، می‌بینیم که موازی‌سازی سرعت اجرا را با ضریب 2 یا 3 افزایش می‌دهد.
709+
در ایستگاه کاری ما، می‌بینیم که موازی‌سازی در اینجا افزایش سرعت متوسط اما ارزشمندی فراهم می‌کند.
646710

647-
(اگر به صورت محلی اجرا می‌کنید، اعداد متفاوتی خواهید گرفت که عمدتاً به تعداد CPUها در دستگاه شما بستگی دارد.)
711+
(اگر به صورت محلی اجرا می‌کنید، نتایج متفاوتی خواهید گرفت که عمدتاً به تعداد CPUها در دستگاه شما بستگی دارد.)
712+
713+
توجه کنید که ما همه نقاط تصادفی را *قبل از* حلقه کشیدیم و آن‌ها را به صورت آرایه عبور دادیم، بنابراین حلقه موازی فقط از حافظه *می‌خواند*.
714+
715+
کشیدن نقاط *درون* حلقه موازی به جای این کار به طرز شگفت‌انگیزی حساس است.
716+
717+
ما بررسی می‌کنیم چرا این‌طور است، و چگونه می‌توان آن را با ایمنی انجام داد، در {ref}`numba_ex_race`.
718+
719+
```{solution-end}
720+
```
721+
722+
723+
```{exercise}
724+
:label: numba_ex_race
725+
726+
در {ref}`numba_ex3` ما همه نقاط تصادفی را *قبل از* حلقه موازی کشیدیم.
727+
728+
وسوسه‌انگیز است که به جای آن هر نقطه را *درون* حلقه `prange` بکشیم، با عبور دادن یک `rng` تولیدکننده به عنوان آرگومان و فراخوانی `rng.uniform()` در بدنه حلقه.
729+
730+
آن را امتحان کنید: کد باید اجرا شود و عددی نزدیک به $\pi$ برگرداند، با این حال یک اشکال ظریف در این رویکرد وجود دارد.
731+
732+
به این صورت بررسی کنید:
733+
734+
1. تابع خود را چند بار با *همان* seed فراخوانی کنید و بررسی کنید آیا نتیجه تکرارپذیر است.
735+
2. تخمین را بارها در طیفی از اندازه‌های نمونه تکرار کنید و پراکندگی آن را با یک نسخه موازی درست مقایسه کنید.
736+
737+
سپس توضیح دهید چه چیزی اشتباه پیش می‌رود و راهی درست برای کشیدن درون یک حلقه موازی ارائه دهید.
738+
739+
راهنمایی: سعی کنید از یک تابع تصادفی قدیمی مانند `np.random.uniform()` به جای یک `Generator` استفاده کنید و ببینید چه اتفاقی می‌افتد.
740+
```
741+
742+
```{solution-start} numba_ex_race
743+
:class: dropdown
744+
```
745+
746+
در اینجا نسخه وسوسه‌انگیز است.
747+
748+
ما `rng` را به عنوان آرگومان عبور می‌دهیم و آن را درون حلقه `prange` فراخوانی می‌کنیم.
749+
750+
```{code-cell} ipython3
751+
n = 1_000_000
752+
rng = np.random.default_rng()
753+
754+
@jit(parallel=True)
755+
def calculate_pi_in_loop_parallel(rng, n):
756+
count = 0
757+
for i in prange(n):
758+
u, v = rng.uniform(), rng.uniform()
759+
d = np.sqrt((u - 0.5)**2 + (v - 0.5)**2)
760+
if d < 0.5:
761+
count += 1
762+
return (count / n) * 4
763+
764+
calculate_pi_in_loop_parallel(rng, n)
765+
```
766+
767+
کد بدون خطا اجرا می‌شود و چیزی نزدیک به $\pi$ برمی‌گرداند.
768+
769+
اما چیزی به طور خاموش با نتایج اشتباه است.
770+
771+
در اینجا، هر نخ از *همان* تولیدکننده `rng` می‌کشد.
772+
773+
یک تولیدکننده هر عدد را با به‌روزرسانی یک حالت داخلی تولید می‌کند.
774+
775+
تحت `prange`، بسیاری از نخ‌ها آن یک حالت مشترک را به طور همزمان می‌خوانند و به‌روز می‌کنند، بدون هیچ هماهنگی بین آن‌ها.
776+
777+
این یک [**رقابت داده (data race)**](https://docs.oracle.com/cd/E19205-01/820-0619/geojs/index.html) است.
778+
779+
این همبستگی‌هایی بین کشیدن‌ها ایجاد می‌کند و حتی می‌تواند باعث شود برخی کشیدن‌ها به طور غیرقابل‌پیش‌بینی تکرار شوند.
780+
781+
دو نشانه این مشکل را آشکار می‌کند.
782+
783+
*نشانه ۱: نتیجه دیگر تکرارپذیر نیست.*
784+
785+
یک تولیدکننده درست هر بار که همان seed به آن داده شود، همان پاسخ را برمی‌گرداند.
786+
787+
به دلیل رقابت داده، ترتیبی که نخ‌ها به طور اتفاقی به حالت مشترک دست می‌زنند بر جریان کشیدن‌ها تأثیر می‌گذارد، بنابراین پاسخ حتی وقتی seed ثابت است تکرارپذیر نیست.
788+
789+
```{code-cell} ipython3
790+
for seed in (1, 1, 1):
791+
print(calculate_pi_in_loop_parallel(np.random.default_rng(seed), n))
792+
```
793+
794+
هر فراخوانی از همان seed استفاده می‌کند، با این حال پاسخ‌ها متفاوت هستند.
795+
796+
*نشانه ۲: تخمین‌گر بسیار پر نویزتر از آن است که باید باشد.*
797+
798+
کشیدن‌های تکراری و همبسته اطلاعات کمتری نسبت به $n$ کشیدن مستقل حمل می‌کنند، بنابراین اندازه نمونه *مؤثر* بسیار کوچک‌تر از $n$ است.
799+
800+
راه‌حل این است که به هر نخ حالت تصادفی خاص خودش را بدهیم، کاری که توابع قدیمی NumPy مانند `np.random.uniform()` به طور خودکار تحت Numba انجام می‌دهند.
801+
802+
```{code-cell} ipython3
803+
@jit(parallel=True)
804+
def calculate_pi_legacy(n):
805+
count = 0
806+
for i in prange(n):
807+
u, v = np.random.uniform(0, 1), np.random.uniform(0, 1)
808+
d = np.sqrt((u - 0.5)**2 + (v - 0.5)**2)
809+
if d < 0.5:
810+
count += 1
811+
return (count / n) * 4
812+
```
813+
814+
برای دیدن هزینه این رقابت، هر تخمین را بارها تکرار می‌کنیم و پراکندگی آن را در برابر نسخه درست با افزایش اندازه نمونه رسم می‌کنیم.
815+
816+
```{code-cell} ipython3
817+
sample_sizes = np.logspace(3, 6, 10).astype(int)
818+
num_reps = 20
819+
820+
methods = [("حالت مخصوص هر نخ (درست)",
821+
lambda n: calculate_pi_legacy(n), 'C0'),
822+
("تولیدکننده مشترک در prange (رقابت داده)",
823+
lambda n: calculate_pi_in_loop_parallel(np.random.default_rng(), n), 'C1')]
824+
825+
fig, ax = plt.subplots()
826+
for label, estimate, color in methods:
827+
draws = np.array([[estimate(int(m)) for _ in range(num_reps)]
828+
for m in sample_sizes])
829+
means, stds = draws.mean(axis=1), draws.std(axis=1)
830+
ax.plot(sample_sizes, means, color=color, marker='o', ms=3, label=label)
831+
ax.fill_between(sample_sizes, means - 2 * stds, means + 2 * stds,
832+
color=color, alpha=0.2)
833+
ax.axhline(np.pi, color='k', lw=0.8, ls='--', label=r'$\pi$')
834+
ax.set_xscale('log')
835+
ax.set_xlabel('تعداد نمونه‌ها')
836+
ax.set_ylabel(r'تخمین $\pi$')
837+
ax.legend()
838+
plt.show()
839+
```
840+
841+
هر دو نوار حول $\pi$ متمرکز هستند، اما نوار مرتبط با رقابت داده پهن‌تر از نوار دیگر است و به آرامی با افزایش اندازه نمونه باریک می‌شود.
842+
843+
گزینه ایمن دیگر همان است که در {ref}`numba_ex3` بود: نقاط را قبل از حلقه بکشید تا حلقه موازی فقط از حافظه بخواند.
844+
845+
```{solution-end}
846+
```
847+
848+
849+
```{exercise}
850+
:label: numba_ex_draw_speed
851+
852+
اکنون دو راه درست برای تخمین $\pi$ به صورت موازی داریم.
853+
854+
یکی همه نقاط را *قبل از* حلقه می‌کشد، مانند {ref}`numba_ex3`.
855+
856+
دیگری آن‌ها را *درون* حلقه با توابع قدیمی می‌کشد، مانند {ref}`numba_ex_race`.
857+
858+
سرعت آن‌ها را در `n = 100_000_000` مقایسه کنید، از جمله زمان صرف‌شده برای تولید نقاط تصادفی.
859+
```
860+
861+
```{solution-start} numba_ex_draw_speed
862+
:class: dropdown
863+
```
864+
865+
ما هر رویکرد را از ابتدا تا انتها زمان‌بندی می‌کنیم، بنابراین نسخه پیش‌کشیدن هزینه ساخت آرایه‌های خود را می‌پردازد.
866+
867+
```{code-cell} ipython3
868+
n = 100_000_000
869+
rng = np.random.default_rng()
870+
871+
with qe.Timer():
872+
u_draws = rng.uniform(size=n)
873+
v_draws = rng.uniform(size=n)
874+
calculate_pi_parallel(u_draws, v_draws)
875+
```
876+
877+
```{code-cell} ipython3
878+
with qe.Timer():
879+
calculate_pi_legacy(n)
880+
```
881+
882+
کشیدن درون حلقه بسیار سریع‌تر است.
883+
884+
نسخه پیش‌کشیدن دو آرایه خود را روی یک نخ واحد قبل از شروع حلقه تولید می‌کند.
885+
886+
نسخه درون حلقه در عوض تولید اعداد تصادفی را در همه نخ‌ها پخش می‌کند.
887+
888+
همچنین از تخصیص دو آرایه از `n` عدد اجتناب می‌کند، بنابراین هم زمان و هم حافظه صرفه‌جویی می‌کند.
648889

649890
```{solution-end}
650891
```
@@ -667,8 +908,8 @@ $$
667908
668909
1. $\beta$ یک فاکتور تنزیل است،
669910
2. $n$ تاریخ انقضا است،
670-
2. $K$ قیمت اعمال است و
671-
3. $\{S_t\}$ قیمت دارایی پایه در هر زمان $t$ است.
911+
3. $K$ قیمت اعمال است و
912+
4. $\{S_t\}$ قیمت دارایی پایه در هر زمان $t$ است.
672913
673914
فرض کنید که `n, β, K = 20, 0.99, 100`.
674915
@@ -722,6 +963,16 @@ $$
722963

723964
با استفاده از این واقعیت، راه‌حل را می‌توان به شرح زیر نوشت.
724965

966+
```{note}
967+
در اینجا ما کشیدن‌های تصادفی را درون حلقه داخلی نگه می‌داریم و از API قدیمی
968+
`np.random.randn()` به جای یک `Generator` استفاده می‌کنیم.
969+
970+
این به این دلیل است که پشتیبانی Numba از اشیاء `Generator` تحت اجرای موازی
971+
(`@jit(parallel=True)`) [ایمن در برابر نخ (thread-safe)](https://numba.readthedocs.io/en/stable/reference/numpysupported.html#generator-objects) نیست.
972+
973+
پیش‌کشیدن شوک‌ها در آرایه‌هایی با شکل `(M, n)` از این مشکل اجتناب می‌کند اما در اینجا غیرعملی است، زیرا `M = 10_000_000` به چندین گیگابایت حافظه نیاز خواهد داشت.
974+
```
975+
725976

726977
```{code-cell} ipython3
727978
M = 10_000_000

0 commit comments

Comments
 (0)