σμ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 AcademyRoadmapsSkills
Ctrl K
AO
+1,2 Tsd. Lernende
AnmeldenBeitreten ↗
RoadmapsSkills
AO
+1,2 Tsd. Lernende
Anmelden
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
σμ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 AcademyRoadmapsSkills
Ctrl K
AO
+1,2 Tsd. Lernende
AnmeldenBeitreten ↗
RoadmapsSkills
AO
+1,2 Tsd. Lernende
Anmelden
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Cache locality-Skill›Modul 3 · Die ganze Maschine›Thema 3.4·~1 Std.Fortgeschritten

NUMA-Effekte

Position in der SkillModul 3 von 3 · Thema 4 von 7
3.4 NUMA-Effekte
—Lab-Punkte— / 100XP gesamt—Serie—

5 Min. Lesezeit3 Codebeispiele

  1. 01Speicher mit Entfernung
  2. 02Wer entscheidet, wo eine Seite liegt
  3. 03Lokale und entfernte Zugriffe zählen
  4. 04Was Sie bauen werden

Auf einem Server mit zwei Sockeln kann derselbe Lesezugriff 80 ns oder 130 ns kosten, je nachdem, auf welchem Sockel die Seite liegt. Wo eine Seite liegt, wird bei der ersten Berührung entschieden. Diese Lektion modelliert zwei Knoten, zeigt, wie ein einzelner Thread, der die Daten initialisiert, die Hälfte der Arbeit entfernt macht, und behebt es, indem jeder Thread zuerst seinen eigenen Teil berührt.

01

Speicher mit Entfernung

Ein großer Server hat zwei Prozessorsockel, und jeder Sockel hat eigene Speichermodule. Ein Kern liest den Speicher seines Sockels direkt. Um den Speicher des anderen Sockels zu lesen, überquert die Anfrage eine Verbindung zwischen den Sockeln, was länger dauert. Eine solche Maschine ist NUMA. Jeder Sockel mit seinem Speicher bildet einen Knoten, und der Knoten eines Threads ist der, auf dem sein Kern sitzt. In unserem Modell gibt es zwei Knoten mit je zwei Threads: Threads 0 und 1 auf Knoten 0, Threads 2 und 3 auf Knoten 1. Ein lokaler Zugriff kostet 80 ns, ein entfernter 130 ns.

main.pyPython
print([node_of_thread(thread, 2) for thread in range(4)])for thread_node in (0, 1):    print("thread on node", thread_node, [LOCAL_NS if page_node == thread_node else REMOTE_NS for page_node in (0, 1)])
[0, 0, 1, 1]
thread on node 0 [80.0, 130.0]
thread on node 1 [130.0, 80.0]

Jede Zeile ist die Sicht eines Threads: Er zahlt 80 ns für Seiten auf seinem eigenen Knoten und 130 ns für Seiten auf dem anderen. Der Unterschied von 50 ns wirkt klein, gilt aber für jeden Zugriff, der den Speicher erreicht.

02

Wer entscheidet, wo eine Seite liegt

Das Betriebssystem gibt einem Programm Speicher in Seiten und wählt den Knoten einer Seite im Moment ihrer ersten Nutzung. Die Standardregel ist First Touch: Die Seite kommt auf den Knoten des Threads, der sie zuerst berührt, und bleibt dort. Eine Alternative ist, die Seiten über die Knoten zu verschränken. Der Code unten wendet beide Regeln auf eine kurze Liste von Zugriffen an, geschrieben als (Thread, Adresse).

main.pyPython
def first_touch_placement(trace, threads_per_node):    placement = {}    for thread, address in trace:        page = address // PAGE_BYTES        if page not in placement:            placement[page] = node_of_thread(thread, threads_per_node)    return placementdef interleaved_placement(trace, n_nodes):    return {page: page % n_nodes for page in {address // PAGE_BYTES for _, address in trace}}trace = [(0, 0), (0, 4096), (3, 8192), (1, 4100)]print(first_touch_placement(trace, 2))print(interleaved_placement(trace, 2))
{0: 0, 1: 0, 2: 1}
{0: 0, 1: 1, 2: 0}

Die Seiten 0 und 1 wurden zuerst von Thread 0 berührt, der auf Knoten 0 sitzt, sie kommen also auf Knoten 0. Seite 2 wurde zuerst von Thread 3 auf Knoten 1 berührt, sie kommt also auf Knoten 1. Der spätere Zugriff von Thread 1 auf Seite 1 verschiebt sie nicht. Das Verschränken ignoriert, wer was berührt: Seite 1 kommt auf Knoten 1, einfach weil 1 modulo 2 gleich 1 ist.

03

Lokale und entfernte Zugriffe zählen

Ein typischer paralleler Job teilt eine Tabelle in Blöcke, einen für jeden Thread. Jeder Thread liest dann nur seinen Block, idealerweise liegt also jede Seite, die er liest, auf seinem eigenen Knoten. Die Funktionen unten bauen dieses Zugriffsmuster für 400 Datensätze auf vier Threads und zählen die lokalen und entfernten Zugriffe und ihre Zeit für drei Platzierungen. In der ersten, „single“, hat ein einziger Thread alle Daten initialisiert, sodass First Touch jede Seite auf Knoten 0 gelegt hat. In der zweiten haben die Besitzer ihre eigenen Blöcke initialisiert. In der dritten sind die Seiten verschränkt.

main.pyPython
def access_costs(trace, placement, threads_per_node):    local = 0    remote = 0    for thread, address in trace:        if placement[address // PAGE_BYTES] == node_of_thread(thread, threads_per_node):            local += 1        else:            remote += 1    return {"local": local, "remote": remote, "ns": local * LOCAL_NS + remote * REMOTE_NS}def partition_trace(n_records, n_threads, record_bytes):    chunk = n_records // n_threads    return [(thread, record * record_bytes) for thread in range(n_threads) for record in range(thread * chunk, (thread + 1) * chunk)]work = partition_trace(400, 4, 512)single = first_touch_placement([(0, address) for _, address in work], 2)owners = first_touch_placement(work, 2)spread = interleaved_placement(work, 2)for name, placement in (("single", single), ("owners", owners), ("interleaved", spread)):    print(name, access_costs(work, placement, 2))
single {'local': 200, 'remote': 200, 'ns': 42000.0}
owners {'local': 400, 'remote': 0, 'ns': 32000.0}
interleaved {'local': 208, 'remote': 192, 'ns': 41600.0}
average access time = local share × 80 ns + remote share × 130 ns(1)bei der Hälfte entfernter Zugriffe: 0,5 × 80 + 0,5 × 130 = 105 ns statt 80 ns

Wenn ein einziger Thread alles initialisiert, ist die Hälfte der Zugriffe entfernt, und der Job braucht 42.000 ns für 400 Lesezugriffe. Initialisieren die Besitzer ihre Blöcke, ist nichts entfernt, und es dauert 32.000 ns, etwa 1,3-mal schneller bei exakt demselben Programm. Das Verschränken hilft hier nicht: Da jeder Thread einen zusammenhängenden Block liest, legen die verschränkten Seiten die Hälfte seiner Daten auf den falschen Knoten. Verschränken ist für Daten, die jeder Thread überall liest, wo keine Platzierung für alle gut sein kann.

In einem Thread zu initialisieren ist der übliche Fehler

Ein Hauptthread, der ein großes Array anlegt und füllt, bevor er die Worker startet, entscheidet die Platzierung jeder Seite, auf seinem eigenen Knoten. Das Programm ist korrekt und der Profiler zeigt nichts Falsches, nur eine Verlangsamung, die mit der Zahl der Sockel wächst. Lassen Sie jeden Worker den Teil initialisieren, den er nutzen wird.

Werkzeuge, die die Platzierung steuern

Unter Linux verteilt numactl --interleave=all die Seiten eines Programms über die Knoten, numactl --cpunodebind und --membind binden Threads und Speicher an Knoten, und numastat zeigt, wie viele Zugriffe lokal waren. Trading-Systeme binden meist jeden Thread an einen Kern und geben ihm Speicher von seinem eigenen Knoten.

Was die echten Daten sagen4.000 SPY-Datensätze zu 512 Byte (500 Seiten), 4 Threads auf 2 Knoten, 80 ns lokal, 130 ns entfernt
Von einem Thread initialisiert420,000ns2.000 entfernte Zugriffe von 4.000
Von den Besitzern initialisiert320,000nsjeder Zugriff ist lokal
Verschränkte Seiten420,000nsdie Hälfte der Daten auf dem falschen Knoten
Gewinn durch First Touch der Besitzer1.31×dasselbe Programm, dieselben Daten
Zeit der 4.000 Lesezugriffe, in Mikrosekunden
Ein Thread420
Besitzer320
Verschränkt420
04

Was Sie bauen werden

Das Labor folgt der Lektion: der Knoten eines Threads, die zwei Platzierungsregeln, das Zugriffsmuster eines partitionierten Jobs, die Zählung lokaler und entfernter Zugriffe und die Studie an den SPY-Datensätzen.

  • Funktion · FrageBaut auf
  • node_of_thread(thread, threads_per_node)Auf welchem Knoten läuft ein Thread?Die ganzzahlige Division
  • first_touch_placement(trace, threads_per_node)Wohin legt First Touch jede Seite?node_of_thread
  • interleaved_placement(trace, n_nodes)Und das Verschränken?Einfache Arithmetik
  • partition_trace(n_records, n_threads, record_bytes)Welche Zugriffe macht jeder Thread?Einfache Arithmetik
  • access_costs(trace, placement, threads_per_node)Wie viele lokale und entfernte Zugriffe, und wie lange?node_of_thread
  • study(bars)Was kostet eine falsche Platzierung bei den SPY-Datensätzen?partition_tracefirst_touch_placementaccess_costs

Das Wichtigste

  • Speicher hat eine Entfernung. Auf einem Mehrsockel-Server kostet eine Seite auf dem anderen Knoten 130 ns statt 80 ns, die Platzierung gehört also zur Leistung.
  • First Touch entscheidet. Eine Seite liegt dort, wo ihr erster Berührer läuft; ein Hauptthread, der alles füllt, lässt die Hälfte der Arbeit entfernt.
  • Passen Sie Daten ihrem Leser an. Lassen Sie jeden Worker den Block initialisieren, den er liest: hier 1,3-mal schneller; verschränken Sie nur Daten, die alle Threads überall lesen.
Schnelltest
Jeder von vier Threads liest nur sein eigenes Viertel einer Tabelle. Welche Platzierung ist die beste?
Wähle eine Antwort.

Nächstes Thema: Wo die Bytes liegen, ist nur die halbe Geschichte; auch wie sie vergeben werden, zählt. In Allokatoren und Speicher-Pools fordert ein Programm viele kleine Objekte an, und die Reihenfolge und die Orte, an denen der Allokator sie vergibt, bestimmen, wie viele Cache-Zeilen ein späterer Durchlauf berührt.

Lab: Was bewertet wird

6 Aufgaben · 100 Pkt.
  • Der Knoten eines Threads5 Pkt.
  • First-Touch-Platzierung20 Pkt.
  • Verschränkte Platzierung10 Pkt.
  • Ein partitionierter Job15 Pkt.
  • Lokale und entfernte Zugriffe25 Pkt.
  • Die Studie mit der SPY-Tabelle25 Pkt.
Explorer · lab-cl-10
▾ lab-cl-10
▾ data
…
bash · im Browser
learner@quantcraft:~/lab-cl-10$
Python noch nicht geladenPythonLn 1, Col 1Spaces: 4UTF-8

Dieses Thema zurücksetzen?

Alles in lab-cl-10 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