σμ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 AcademyFeuilles de routeParcours
Ctrl K
AO
+1,2 k apprenants
ConnexionRejoindre ↗
Feuilles de routeParcours
AO
+1,2 k apprenants
Connexion
  • 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 AcademyFeuilles de routeParcours
Ctrl K
AO
+1,2 k apprenants
ConnexionRejoindre ↗
Feuilles de routeParcours
AO
+1,2 k apprenants
Connexion
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Parcours Cache locality›Module 3 · La machine au sens large›Sujet 3.4·~1 hAvancé

Effets NUMA

Position dans le parcoursModule 3 sur 3 · Sujet 4 sur 7
3.4 Effets NUMA
—Score du lab— / 100XP total—Série—

5 min de lecture3 exemples de code

  1. 01Une mémoire avec une distance
  2. 02Qui décide où vit une page
  3. 03Compter les accès locaux et distants
  4. 04Ce que vous allez construire

Sur un serveur à deux sockets, la même lecture peut coûter 80 ns ou 130 ns, selon le socket où la page se trouve. L'endroit où vit une page est décidé à sa première utilisation. Ce cours modélise deux nœuds, montre comment un seul thread qui initialise les données rend la moitié du travail distante, et corrige cela en laissant chaque thread toucher d'abord sa propre part.

01

Une mémoire avec une distance

Un gros serveur a deux sockets de processeur, et chaque socket a ses propres barrettes de mémoire. Un cœur lit directement la mémoire de son socket. Pour lire celle de l'autre socket, la requête traverse un lien entre les sockets, ce qui prend plus de temps. Une telle machine est NUMA. Chaque socket et sa mémoire forment un nœud, et le nœud d'un thread est celui où se trouve son cœur. Dans notre modèle, il y a deux nœuds de deux threads chacun : les threads 0 et 1 sur le nœud 0, les threads 2 et 3 sur le nœud 1. Un accès local coûte 80 ns et un accès distant 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]

Chaque ligne est la vue d'un thread : il paie 80 ns pour les pages de son propre nœud et 130 ns pour celles de l'autre. La différence, 50 ns, paraît petite, mais elle s'applique à chaque accès qui atteint la mémoire.

02

Qui décide où vit une page

Le système d'exploitation donne de la mémoire à un programme par pages, et il choisit le nœud d'une page au moment où elle est utilisée pour la première fois. La règle par défaut est la première touche : la page va sur le nœud du thread qui la touche en premier, et elle y reste. Une alternative est d'entrelacer les pages sur les nœuds. Le code ci-dessous applique les deux règles à une courte liste d'accès écrits (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}

Les pages 0 et 1 ont été touchées d'abord par le thread 0, qui est sur le nœud 0 : elles vont donc sur le nœud 0. La page 2 a été touchée d'abord par le thread 3, sur le nœud 1 : elle va sur le nœud 1. L'accès ultérieur du thread 1 à la page 1 ne la déplace pas. L'entrelacement ignore qui touche quoi : la page 1 va sur le nœud 1 simplement parce que 1 modulo 2 vaut 1.

03

Compter les accès locaux et distants

Un travail parallèle typique découpe une table en morceaux, un par thread. Chaque thread ne lit ensuite que son morceau : idéalement, chaque page qu'il lit est sur son propre nœud. Les fonctions ci-dessous construisent ce motif d'accès pour 400 enregistrements sur quatre threads, et comptent les accès locaux et distants et leur temps pour trois placements. Dans le premier, « single », un seul thread a initialisé toutes les données, donc la première touche a mis chaque page sur le nœud 0. Dans le deuxième, les propriétaires ont initialisé leurs propres morceaux. Dans le troisième, les pages sont entrelacées.

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)avec la moitié des accès distants : 0,5 × 80 + 0,5 × 130 = 105 ns au lieu de 80 ns

Avec un seul thread qui initialise tout, la moitié des accès sont distants et le travail prend 42 000 ns pour 400 lectures. Quand les propriétaires initialisent leurs morceaux, rien n'est distant et cela prend 32 000 ns, environ 1,3 fois plus vite, pour exactement le même programme. L'entrelacement n'aide pas ici : comme chaque thread lit un morceau contigu, les pages entrelacées mettent la moitié de ses données sur le mauvais nœud. L'entrelacement sert pour des données que tous les threads lisent partout, où aucun placement ne peut convenir à tous.

Initialiser dans un seul thread est l'erreur habituelle

Un thread principal qui alloue et remplit un gros tableau avant de lancer les workers décide du placement de chaque page, sur son propre nœud. Le programme est correct et le profileur ne montre rien d'anormal, seulement un ralentissement qui croît avec le nombre de sockets. Laissez chaque worker initialiser la partie qu'il utilisera.

Des outils qui contrôlent le placement

Sous Linux, numactl --interleave=all répartit les pages d'un programme sur les nœuds, numactl --cpunodebind et --membind attachent threads et mémoire à des nœuds, et numastat montre combien d'accès étaient locaux. Les systèmes de trading attachent en général chaque thread à un cœur et lui donnent la mémoire de son propre nœud.

Ce que disent les vraies données4 000 enregistrements SPY de 512 octets (500 pages), 4 threads sur 2 nœuds, 80 ns local, 130 ns distant
Initialisé par un seul thread420,000ns2 000 accès distants sur 4 000
Initialisé par les propriétaires320,000nstous les accès sont locaux
Pages entrelacées420,000nsla moitié des données sur le mauvais nœud
Gain de la première touche par les propriétaires1.31×le même programme, les mêmes données
Temps des 4 000 lectures, en microsecondes
Un thread420
Propriétaires320
Entrelacé420
04

Ce que vous allez construire

Le labo suit le cours : le nœud d'un thread, les deux règles de placement, le motif d'accès d'un travail partitionné, le décompte des accès locaux et distants, et l'étude sur les enregistrements SPY.

  • Fonction · questionS'appuie sur
  • node_of_thread(thread, threads_per_node)Sur quel nœud tourne un thread ?La division entière
  • first_touch_placement(trace, threads_per_node)Où la première touche place-t-elle chaque page ?node_of_thread
  • interleaved_placement(trace, n_nodes)Et l'entrelacement ?De l'arithmétique simple
  • partition_trace(n_records, n_threads, record_bytes)Quels accès chaque thread fait-il ?De l'arithmétique simple
  • access_costs(trace, placement, threads_per_node)Combien d'accès locaux et distants, et combien de temps ?node_of_thread
  • study(bars)Que coûte un mauvais placement sur les enregistrements SPY ?partition_tracefirst_touch_placementaccess_costs

À retenir

  • La mémoire a une distance. Sur un serveur multi-sockets, une page de l'autre nœud coûte 130 ns au lieu de 80 ns : le placement fait partie de la performance.
  • La première touche décide. Une page vit là où tourne son premier toucheur ; un thread principal qui remplit tout laisse la moitié du travail distant.
  • Faites correspondre les données et leur lecteur. Laissez chaque worker initialiser le morceau qu'il lit : 1,3 fois plus rapide ici ; n'entrelacez que les données que tous les threads lisent partout.
Vérification rapide
Chacun de quatre threads ne lit que son propre quart d'une table. Quel placement est le meilleur ?
Choisissez une réponse.

Sujet suivant : savoir où vivent les octets n'est que la moitié de l'histoire ; la façon dont on les distribue compte aussi. Dans Allocateurs et pools mémoire, un programme demande beaucoup de petits objets, et l'ordre et les endroits où l'allocateur les distribue décident du nombre de lignes de cache qu'un parcours ultérieur touche.

Lab : ce qui est noté

6 tâches · 100 pts
  • Le nœud d'un thread5 pts
  • Placement par première touche20 pts
  • Placement entrelacé10 pts
  • Un travail partitionné15 pts
  • Accès locaux et distants25 pts
  • L'étude sur la table SPY25 pts
Explorateur · lab-cl-10
▾ lab-cl-10
▾ data
…
bash · dans le navigateur
learner@quantcraft:~/lab-cl-10$
Python pas encore chargéPythonLn 1, Col 1Spaces: 4UTF-8

Réinitialiser ce sujet ?

Tout le contenu de lab-cl-10 revient aux fichiers de départ et le terminal est vidé. Votre solution actuelle sera perdue : cette action est irréversible.

Sera réinitialisé
  • main.py → code de départ
  • Historique du terminal et sortie des tests
  • Modifications non soumises
Sera conservé
  • Meilleur score du lab
  • Votre XP et votre série
  • Progression du cours et notes