σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademySkills
Ctrl K
AO
+1,2 Tsd. Lernende
AnmeldenBeitreten ↗
Skills
AO
+1,2 Tsd. Lernende
Anmelden
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python-Skill›Modul 10 · Hochleistungs-Python-Entwicklung›Thema 10.10·~55 Min.Fortgeschritten

Multiprocessing

Position in der SkillModul 10 von 11 · Thema 10 von 13
10.10 Multiprocessing
—Lab-Punkte— / 100XP gesamt—Serie—

Getrennte Prozesse, getrennter Speicher

Threads können CPU-gebundenes Python wegen der GIL nicht beschleunigen, Prozesse aber schon: Jeder ist ein vollständiger Python-Interpreter mit eigener GIL, und das Betriebssystem führt sie auf verschiedenen Kernen aus. Das Modul multiprocessing und das freundlichere concurrent.futures.ProcessPoolExecutor starten einen Pool von Arbeitsprozessen und geben jedem einen Teil der Arbeit. Ein Backtest über tausend Parametersätze oder ein Monte-Carlo mit Millionen Pfaden kann alle acht Kerne eines Laptops nutzen und in etwa einem Achtel der Zeit fertig sein.

# A pool of processes: each gets a slice of the work, computed on its own core
from concurrent.futures import ProcessPoolExecutor

def sum_of_squares(bounds):
    start, stop = bounds
    return sum(i * i for i in range(start, stop))

if __name__ == "__main__":                           # required on Windows and macOS, where workers re-import this file
    bounds = [(0, 250_000), (250_000, 500_000), (500_000, 750_000), (750_000, 1_000_000)]
    with ProcessPoolExecutor(max_workers=4) as pool:
        partials = list(pool.map(sum_of_squares, bounds))
    print(sum(partials))                             # 333332833333500000
    print(sum(partials) == sum(i * i for i in range(1_000_000)))        # True  the same answer as one process

Was die Grenze überquert: Kopien, Pickling, Teile

Prozesse teilen keine Variablen. Alles, was Sie an einen Worker senden, die Funktion und ihre Argumente, wird gepickelt (zu Bytes serialisiert), kopiert und wieder aufgebaut, und das Ergebnis kommt auf dem gleichen Weg zurück. Es können also nur picklebare Dinge die Grenze überqueren: Funktionen auf Modulebene und einfache Daten ja, Lambdas, verschachtelte Funktionen, Locks und Generatoren nein, was mit PicklingError oder AttributeError scheitert. Kopieren kostet: Senden Sie wenig und rechnen Sie viel. Geben Sie jedem Worker ein Indexpaar (start, stop) statt einer Datenkopie, wenn er die Daten selbst laden oder erzeugen kann, verteilen Sie wenige große Teile statt einer Million winziger Aufgaben und liefern Sie kleine Ergebnisse wie eine Summe zurück, kein ganzes Array.

# Index pairs instead of data, and a check that something can be sent to another process
import pickle

def chunk_bounds(n, parts):
    size, extra = divmod(n, parts)
    bounds, start = [], 0
    for index in range(parts):
        stop = start + size + (1 if index < extra else 0)
        bounds.append((start, stop))
        start = stop
    return bounds

def is_picklable(obj):
    try:
        pickle.dumps(obj)
    except (pickle.PicklingError, AttributeError, TypeError):
        return False
    return True

print(chunk_bounds(1_000_000, 4))                    # [(0, 250000), (250000, 500000), (500000, 750000), (750000, 1000000)]
print(is_picklable({"AAPL": 184.25}), is_picklable(lambda x: x))      # True False

Unabhängige Zufallsströme und das Zusammenführen von Teilergebnissen

Zwei Details entscheiden, ob ein paralleles Monte-Carlo stimmt. Erstens die Zufalls-Seeds: Ein Worker, der mit demselben Seed startet wie ein anderer, erzeugt dieselben „zufälligen“ Zahlen, und seed + 0, seed + 1, ... ergibt Ströme, die sich überlappen oder korrelieren können. np.random.SeedSequence(seed).spawn(n) leitet n statistisch unabhängige Kind-Seeds aus einem Eltern-Seed ab, sodass der ganze Lauf aus einer einzigen Zahl reproduzierbar bleibt. Zweitens das Zusammenführen: Jeder Worker liefert eine kleine Zusammenfassung, die Sie vereinen. Anzahlen addieren sich, Mittelwerte kombinieren sich als mit der Anzahl gewichteter Durchschnitt, und die Varianz des Ganzen muss die Streuung zwischen den Worker-Mittelwerten enthalten, man kann die Varianzen also nicht einfach mitteln.

# Independent seeds for the workers, and merging their partial statistics exactly
import numpy as np

def spawn_seeds(seed, n):
    return [int(child.generate_state(1)[0]) for child in np.random.SeedSequence(seed).spawn(n)]

def combine_mean_var(partials):
    count = sum(n for n, _, _ in partials)
    mean = sum(n * m for n, m, _ in partials) / count
    variance = sum(n * (v + (m - mean) ** 2) for n, m, v in partials) / count
    return count, mean, variance

seeds = spawn_seeds(7, 4)
print(len(set(seeds)), spawn_seeds(7, 4) == seeds)   # 4 True   four different seeds, reproducible from one number

data = np.random.default_rng(seeds[0]).normal(0.0, 1.0, size=1_000)
partials = [(len(p), p.mean(), p.var()) for p in np.array_split(data, 4)]
count, mean, variance = combine_mean_var(partials)
print(count, np.isclose(mean, data.mean()), np.isclose(variance, data.var()))      # 1000 True True
# A parallel Monte Carlo: every worker has its own seed, and the run is still exactly reproducible
from concurrent.futures import ProcessPoolExecutor

import numpy as np

def simulate(job):
    seed, paths = job
    z = np.random.default_rng(seed).standard_normal(paths)
    payoff = np.maximum(100.0 * np.exp(-0.5 * 0.2 ** 2 + 0.2 * z) - 100.0, 0.0)   # at-the-money call, 20% volatility
    return paths, payoff.mean(), payoff.var()

if __name__ == "__main__":
    seeds = [int(child.generate_state(1)[0]) for child in np.random.SeedSequence(2024).spawn(4)]
    jobs = [(seed, 50_000) for seed in seeds]
    with ProcessPoolExecutor(max_workers=4) as pool:
        partials = list(pool.map(simulate, jobs))
    count = sum(n for n, _, _ in partials)
    price = sum(n * mean for n, mean, _ in partials) / count
    print(count, round(price, 1))                    # 200000 7.9   close to the exact Black-Scholes value of about 7.97
    print(partials == [simulate(job) for job in jobs])      # True  the same seeds give the same numbers in one process
Prüfen Sie, ob sich der parallele Lauf lohntProzesse zu starten und Daten zu kopieren kostet Zeit, daher ist eine Aufgabe von wenigen Millisekunden im Pool langsamer als allein. Messen Sie beides mit den Profiling-Werkzeugen von vorhin und erwarten Sie einen Speed-up unter der Kernzahl (wieder Amdahl). Der Speicher vervielfacht sich mit der Zahl der Worker, und Prozesse, die alle dieselbe große Datei lesen, sollten sie einblenden (np.load(..., mmap_mode="r") oder multiprocessing.shared_memory), statt je eine Kopie zu halten. Und sichern Sie den Einstiegspunkt stets mit if __name__ == "__main__":, sonst versucht unter Windows und macOS jeder Worker, seinen eigenen Pool zu starten.
  • Prozesse haben eigene GIL und eigenen Speicher, daher kann CPU-gebundenes Python alle Kerne nutzen; ein Pool (ProcessPoolExecutor) verteilt die Arbeit.
  • Argumente und Ergebnisse werden gepickelt und kopiert: Senden Sie Indexpaare und kleine Ergebnisse in wenigen großen Teilen; Lambdas und verschachtelte Funktionen lassen sich nicht senden.
  • Geben Sie jedem Worker mit SeedSequence.spawn einen unabhängigen Seed, führen Sie Teilstatistiken exakt zusammen, sichern Sie den Einstiegspunkt und messen Sie den echten Speed-up.

Als Nächstes: SQL-Datenbanken, Marktdaten mit Tabellen und Abfragen speichern und abfragen.

Lab: Was bewertet wird

  • Mit chunk_bounds den Bereich in Abschnitte teilen25 Pkt.
  • Mit combine_mean_var Teilstatistiken zusammenführen25 Pkt.
  • Mit spawn_seeds jedem Worker einen eigenen Seed geben25 Pkt.
  • Mit parallel_speedup die Kosten der Parallelisierung beziffern25 Pkt.
Explorer · lab-126-1
▾ lab-126-1
…
bash · im Browser
learner@quantcraft:~/lab-126-1$
Python noch nicht geladenPythonLn 1, Col 1Spaces: 4UTF-8

Dieses Thema zurücksetzen?

Alles in lab-126-1 wird auf die Startdateien zurückgesetzt und das Terminal geleert. Ihre aktuelle Lösung geht verloren – das lässt sich nicht rückgängig machen.

Wird zurückgesetzt
  • main.py → Startcode
  • Terminal-Verlauf und Testausgabe
  • Nicht eingereichte Änderungen
Bleibt erhalten
  • Beste Lab-Punktzahl
  • Ihre XP und Ihre Serie
  • Vorlesungsfortschritt und Notizen

Melde dich an, um deinen Code auszuführen.

Alle Lektionen kannst du kostenlos lesen. Erstelle ein Konto oder melde dich an, um Code auszuführen, Tests zu starten und dieses Lab abzugeben.

  • IDE im Browser mit Bash-Terminal — nichts zu installieren
  • Sofortige Bewertung, XP und dein Platz auf der Skill
Konto erstellenAnmelden
ODER
Weiter mit GitHub