You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
اگر کامپایل 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` رشد نمیکند.
471
530
472
-
بنابراین با افزودن چهار کاراکتر، افزایش سرعت 2 مرتبه بزرگی به دست میآوریم.
531
+
این ممکن است پیشنهاد کند که کشیدن درون حلقه پیشفرض بهتری است.
532
+
533
+
اما همانطور که در {ref}`numba_ex_race` خواهیم دید، کشیدن درون حلقه با موازیسازی به بدی تعامل دارد.
473
534
474
535
```{solution-end}
475
536
```
@@ -535,10 +596,13 @@ p, q = 0.1, 0.2 # احتمال خروج از حالت پایین و بالا ب
535
596
در اینجا نسخه Python خالص تابع است
536
597
537
598
```{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):
539
604
x = np.empty(n, dtype=np.int64)
540
605
x[0] = 1 # در حالت 1 شروع کن
541
-
U = np.random.uniform(0, 1, size=n)
542
606
for t in range(1, n):
543
607
current_x = x[t-1]
544
608
if current_x == 0:
@@ -551,8 +615,7 @@ def compute_series(n):
551
615
بیایید این کد را اجرا کنیم و بررسی کنیم که کسری از زمان صرف شده در حالت پایین حدود 0.666 است
552
616
553
617
```{code-cell} ipython3
554
-
n = 1_000_000
555
-
x = compute_series(n)
618
+
x = compute_series(n, U)
556
619
print(np.mean(x == 0)) # کسری از زمان که x در حالت 0 است
557
620
```
558
621
@@ -562,7 +625,7 @@ print(np.mean(x == 0)) # کسری از زمان که x در حالت 0 است
562
625
563
626
```{code-cell} ipython3
564
627
with qe.Timer():
565
-
compute_series(n)
628
+
compute_series(n, U)
566
629
```
567
630
568
631
بعد بیایید یک نسخه Numba پیادهسازی کنیم که آسان است
با روشن و خاموش کردن موازیسازی (انتخاب `True` یا `False` در annotation `@jit`)، میتوانیم افزایش سرعتی که چندنخی علاوه بر کامپایل JIT فراهم میکند را آزمایش کنیم.
644
708
645
-
در ایستگاه کاری ما، میبینیم که موازیسازی سرعت اجرا را با ضریب 2 یا 3 افزایش میدهد.
709
+
در ایستگاه کاری ما، میبینیم که موازیسازی در اینجا افزایش سرعت متوسط اما ارزشمندی فراهم میکند.
646
710
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 ثابت است تکرارپذیر نیست.
هر دو نوار حول $\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` عدد اجتناب میکند، بنابراین هم زمان و هم حافظه صرفهجویی میکند.
648
889
649
890
```{solution-end}
650
891
```
@@ -667,8 +908,8 @@ $$
667
908
668
909
1. $\beta$ یک فاکتور تنزیل است،
669
910
2. $n$ تاریخ انقضا است،
670
-
2. $K$ قیمت اعمال است و
671
-
3. $\{S_t\}$ قیمت دارایی پایه در هر زمان $t$ است.
911
+
3. $K$ قیمت اعمال است و
912
+
4. $\{S_t\}$ قیمت دارایی پایه در هر زمان $t$ است.
672
913
673
914
فرض کنید که `n, β, K = 20, 0.99, 100`.
674
915
@@ -722,6 +963,16 @@ $$
722
963
723
964
با استفاده از این واقعیت، راهحل را میتوان به شرح زیر نوشت.
724
965
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` به چندین گیگابایت حافظه نیاز خواهد داشت.
0 commit comments