Connect with us

Entwicklung & Code

Datenparallele Typen in C++26: Bedingte Ausführung von Operationen


Leider habe ich in meinem letzten Beitrag „Datenparallele Typen in C++26: ein Beispiel aus der Praxis“ vergessen, eine Funktion der neuen Bibliothek vorzustellen. Das hole ich in diesem Artikel nach.


Rainer Grimm

Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

Das neue Schlüsselwort where erzeugt einen sogenannten Where-Ausdruck. Damit lassen sich die Elemente eines SIMD-Vektors bedingt ansprechen.

Folgendes Beispiel bringt dieses Verhalten auf den Punkt:


// where.cpp

#include 
#include 
#include 
namespace stdx = std::experimental;
 
void println(std::string_view name, auto const& a)
{
    std::cout << name << ": ";
    for (std::size_t i{}; i != std::size(a); ++i)
        std::cout << a[i] << ' ';
    std::cout << '\n';
}
 
template
stdx::simd my_abs(stdx::simd x)
{
    where(x < 0, x) = -x; // Set elements where x is negative to their absolute value       
    return x;
}
 
int main()
{
    const stdx::native_simd a = 1;
    println("a", a);
 
    const stdx::native_simd b([](int i) { return i - 2; });
    println("b", b);
 
    const auto c = a + b;
    println("c", c);
 
    const auto d = my_abs(c);
    println("d", d);
 
}


In der Funktion my_abs kommt die where-Funktion zum Einsatz: where(x < 0, x) = -x; bewirkt, dass alle Elemente des SIMD-Vektors, die kleiner als Null sind, auf ihren absoluten Wert gesetzt werden.


Beispielcode-Ausgabe

Beispielcode-Ausgabe

Der Screenshot zeigt die Ausgabe des Beispielcodes.

In diesem Fall kommen SSE2-Befehle zum Einsatz. Der SIMD-Vektor ist 128 Bit groß.

Die where-Expression kann mit einem bool-Ausdruck oder einer simd_mask parametrisiert werden.

Obiges Codebeispiel lässt sich auch mit einer simd_mask implementieren. Folgender Code zeigt die Umsetzung:


// whereMask.cpp

#include 
#include 
#include 
namespace stdx = std::experimental;
 
void println(std::string_view name, auto const& a)
{
    std::cout << std::boolalpha << name << ": ";
    for (std::size_t i{}; i != std::size(a); ++i)
        std::cout << a[i] << ' ';
    std::cout << '\n';
}

 
int main()
{
    const stdx::native_simd a = 1;
    println("a", a);
 
    const stdx::native_simd b([](int i) { return i - 2; });
    println("b", b);
 
    const auto c = a + b;
    println("c", c);
 
    const stdx::native_simd_mask x = c < 0; 
    println("x", x);

    auto d  = c;
    where(x, d) *= -1; 
    println("d", d);
 
}


Beginnen möchte ich meine Erläuterung mit den letzten fünf Zeilen der Main-Funktion. Zuerst erzeuge ich die simd_mask x, indem das Prädikat c < 0 auf jedes Element des SIMD-Vektors c angewendet wird.

Die Maske x hat die gleiche Länge wie der SIMD- Vektor, besitzt aber nur Wahrheitswerte. Damit diese Wahrheitswerte als true oder false und nicht als 1 oder 0 dargestellt werden, habe ich der Funktion println den Streammanipulator std::boolalpha hinzugefügt.

Zusätzlich muss ich den SIMD-Vektor d mit c initialisieren, da c konstant ist. Nun lässt sich die Expression where(x, d) *= -1; auf d anwenden. Dabei wird jedes Element des SIMD-Vektors negiert, wenn die Maske den Wert true besitzt.


Screenshot Ausgabe des zweiten Codebeispiels

Screenshot Ausgabe des zweiten Codebeispiels

Der Screenshot zeigt die Ausgabe des Codes mit simd_mask.

Der Datentyp simd_mask ist dem Datentyp simd sehr ähnlich. Der wesentliche Unterschied besteht darin, dass simd alle Standard-Ganzzahltypen, Zeichentypen und die Typen float und double annehmen kann. Im Gegensatz dazu unterstützt simd_mask nur Wahrheitswerte.

Die Definition von simd_mask sieht folgendermaßen aus:


template
class basic_simd_mask


Der Abi-Tag bestimmt die Anzahl der Elemente und deren Speicherplatz. Zur Vollständigkeit sind hier noch einmal die ABI-Tags:

  • scalar: Speichern eines einzelnen Elements
  • fixed_size: Speichern einer bestimmten Anzahl von Elementen
  • compatible: gewährleistet ABI-Kompatibilität
  • native: am effizientesten
  • max_fixed_size: maximale Anzahl von Elementen, die von fixed_size garantiert unterstützt werden

Entsprechend zu simd besitzt simd_mask auch zwei Aliase:


template< size_t Bytes, int N >
using fixed_size_simd_mask = simd_mask>

template< size_t Bytes >
using native_simd_mask = simd_mask>


In meinem vorerst letzten Artikel über data-parallel types möchte ich auf die besonderen Funktionen dafür eingehen.


(rme)



Source link

Entwicklung & Code

Künstliche Neuronale Netze im Überblick 7: Rekursive neuronale Netze


Neuronale Netze sind der Motor vieler Anwendungen in KI und GenAI. Diese Artikelserie gibt einen Einblick in die einzelnen Elemente. Der siebte Teil widmet sich rekursiven neuronalen Netzen, nachdem der sechste Teil der Serie Convolutional Neural Networks vorgestellt hat.


Michael Stal

Michael Stal

Prof. Dr. Michael Stal arbeitet seit 1991 bei Siemens Technology. Seine Forschungsschwerpunkte umfassen Softwarearchitekturen für große komplexe Systeme (Verteilte Systeme, Cloud Computing, IIoT), Eingebettte Systeme und Künstliche Intelligenz.

Er berät Geschäftsbereiche in Softwarearchitekturfragen und ist für die Architekturausbildung der Senior-Software-Architekten bei Siemens verantwortlich.

Rekursive neuronale Netze sind für die Verarbeitung sequenzieller Daten ausgelegt, indem sie einen versteckten Zustand aufrechterhalten, der sich im Laufe der Zeit weiterentwickelt. Im Gegensatz zu Feedforward-Netzwerken, die davon ausgehen, dass jede Eingabe unabhängig von allen anderen ist, ermöglichen rekursive Netzwerke das Speichern von Informationen über Zeiträume hinweg. Bei jedem Schritt t empfängt eine rekurrente Zelle sowohl den neuen Eingabevektor xₜ als auch den vorherigen versteckten Zustand hₜ₋₁. Die Zelle berechnet einen neuen versteckten Zustand hₜ gemäß einer gelernten Transformation und erzeugt (optional) eine Ausgabe yₜ.

In ihrer einfachsten Form berechnet eine Vanilla-RNN-Zelle einen Präaktivierungsvektor zₜ als Summe einer Eingabetransformation und einer versteckten Zustandstransformation plus einer Verzerrung:

zₜ = Wₓ · xₜ + Wₕ · hₜ₋₁ + b

Der neue Zustand entsteht durch elementweise Anwendung einer nicht linearen Aktivierung σ:

hₜ = σ(zₜ)

Wenn bei jedem Zeitschritt eine Ausgabe yₜ erforderlich ist, kann eine Ausleseschicht hinzugefügt werden:

yₜ = V · hₜ + c

wobei V und c eine Ausgabegewichtungsmatrix und ein Bias-Vektor sind.

In PyTorch kapselt die Klasse torch.nn.RNN dieses Verhalten und verarbeitet das Stapeln mehrerer Schichten und Batches nahtlos. Das folgende Beispiel zeigt, wie man eine einlagige RNN-Zelle erstellt, ihr einen Stapel von Sequenzen zuführt und den endgültigen versteckten Zustand extrahiert:


import torch
import torch.nn as nn

# Angenommen, wir haben Sequenzen der Länge 100, jedes Element ist ein 20-dimensionaler Vektor,
# und wir verarbeiten sie in Batches der Größe 16.
seq_len, batch_size, input_size = 100, 16, 20
hidden_size = 50

# Erstellen Sie einen zufälligen Stapel von Eingabesequenzen: Form (seq_len, batch_size, input_size)
inputs = torch.randn(seq_len, batch_size, input_size)

# Instanziieren Sie ein einlagiges RNN mit tanh-Aktivierung (Standard)
rnn = nn.RNN(input_size=input_size,
             hidden_size=hidden_size,
             num_layers=1,
             nonlinearity='tanh',
             batch_first=False)

# Initialisiere den versteckten Zustand: Form (Anzahl_Schichten, Batchgröße, versteckte Größe)
h0 = torch.zeros(1, batch_size, hidden_size)

# Vorwärtspropagierung durch das RNN
outputs, hn = rnn(inputs, h0)

# `outputs` hat die Form (seq_len, batch_size, hidden_size)
# `hn` ist der versteckte Zustand beim letzten Zeitschritt, Form (1, batch_size, hidden_size)


Jede Zeile dieses Ausschnitts hat eine klare Aufgabe. Durch das Erstellen von Eingaben simuliert man ein Batch von Zeitreihendaten. Das RNN-Modul weist zwei Parametermatrizen zu: eine mit der Form (hidden_size, input_size) für Wₓ und eine mit der Form (hidden_size, hidden_size) für Wₕ sowie einen Bias-Vektor der Länge hidden_size. Beim Aufrufen des Moduls für Eingaben und den Anfangszustand h0 durchläuft es die 100 Zeitschritte und berechnet bei jedem Schritt die Rekursionsbeziehung. Der Ausgabetensor sammelt alle Zwischenzustände, während hn nur den letzten zurückgibt.

Obwohl Vanilla-RNNs konzeptionell einfach sind, haben sie Schwierigkeiten, langfristige Abhängigkeiten zu lernen, da über viele Zeitschritte zurückfließende Gradienten dazu neigen, zu verschwinden oder zu explodieren. Um das abzumildern, führen Gated Recurrent Units wie LSTM und GRU interne Gates ein, die steuern, wie stark die Eingabe und der vorherige Zustand den neuen Zustand beeinflussen sollen.

Die LSTM-Zelle (Long Short-Term Memory) verwaltet sowohl einen versteckten Zustand hₜ als auch einen Zellzustand cₜ. Sie verwendet drei Gates – Forget Gate fₜ, Input Gate iₜ und Output Gate oₜ –, die als Sigmoid-Aktivierungen berechnet werden, sowie eine Kandidaten-Zellaktualisierung ĉₜ, die sich mit einer Tanh-Aktivierung berechnen lässt. Konkret:

fₜ = σ( W_f · xₜ + U_f · hₜ₋₁ + b_f )

iₜ = σ( W_i · xₜ + U_i · hₜ₋₁ + b_i )

oₜ = σ( W_o · xₜ + U_o · hₜ₋₁ + b_o )

ĉₜ = tanh( W_c · xₜ + U_c · hₜ₋₁ + b_c )

Der Zellzustand wird dann durch Kombination des vorherigen Zellzustands und des Kandidaten aktualisiert, gewichtet durch die Vergessens- und Eingangsgatter:

cₜ = fₜ * cₜ₋₁ + iₜ * ĉₜ

Schließlich erstellt das System den neuen versteckten Zustand, indem es das Ausgangs-Gate auf die Nichtlinearität des Zellzustands anwendet:

hₜ = oₜ * tanh(cₜ)

PyTorchs torch.nn.LSTM kapselt all diese Berechnungen unter der Haube. Folgender Code zeigt ein Beispiel für eine Reihe von Sequenzen:


import torch
import torch.nn as nn

# Sequenzparameter wie zuvor
seq_len, batch_size, input_size = 100, 16, 20
hidden_size, num_layers = 50, 2

# Zufälliger Eingabebatch
inputs = torch.randn(seq_len, batch_size, input_size)

# Instanziieren eines zweischichtigen LSTM
lstm = nn.LSTM(input_size=input_size,
               hidden_size=hidden_size,
               num_layers=num_layers,
               batch_first=False)

# Initialisiere versteckte und Zellzustände: jeweils mit der Form (Anzahl_Schichten, Batchgröße, versteckte Größe)
h0 = torch.zeros(num_layers, batch_size, hidden_size)
c0 = torch.zeros(num_layers, batch_size, hidden_size)

# Vorwärtsdurchlauf durch das LSTM
outputs, (hn, cn) = lstm(inputs, (h0, c0))

# `outputs` hat die Form (seq_len, batch_size, hidden_size)
# `hn` und `cn` haben jeweils die Form (num_layers, batch_size, hidden_size)


Die Gated Recurrent Unit (GRU) vereinfacht das LSTM, indem sie die Vergessens- und Eingangsgatter zu einem einzigen Aktualisierungsgatter zₜ kombiniert und die Zell- und versteckten Zustände zusammenführt. Die Gleichungen lauten:

zₜ = σ( W_z · xₜ + U_z · hₜ₋₁ + b_z )

rₜ = σ( W_r · xₜ + U_r · hₜ₋₁ + b_r )

ħₜ = tanh( W · xₜ + U · ( rₜ * hₜ₋₁ ) + b )

hₜ = (1 − zₜ) * hₜ₋₁ + zₜ * ħₜ

In PyTorch bietet torch.nn.GRU diese Funktionalität mit derselben Schnittstelle wie nn.LSTM, außer dass nur die versteckten Zustände zurückgegeben werden.

Bei der Arbeit mit Sequenzen variabler Länge benötigt man häufig torch.nn.utils.rnn.pack_padded_sequence und pad_packed_sequence, um Sequenzen effizient im Batch zu verarbeiten, ohne Rechenleistung für das Auffüllen von Tokens zu verschwenden.

Rekursive Netzwerke eignen sich hervorragend für Aufgaben wie Sprachmodellierung, Zeitreihenprognosen und Sequenz-zu-Sequenz-Übersetzungen, wurden jedoch in vielen Anwendungsbereichen von aufmerksamkeitsbasierten Modellen übertroffen.

Zunächst widmet sich der nächste Teil dieser Serie jedoch der Kombination aus konvolutionalen und rekursiven Schichten, um Daten mit sowohl räumlicher als auch zeitlicher Struktur zu verarbeiten. Bei der Videoklassifizierung kann ein Convolutional Neural Network beispielsweise Merkmale auf Frame-Ebene extrahieren, die dann in ein LSTM eingespeist werden, um Bewegungsdynamiken zu erfassen.


(rme)



Source link

Weiterlesen

Entwicklung & Code

EventSourcingDB 1.1 bietet flexiblere Konsistenzsteuerung und signierte Events


Version 1.1 der auf Event Sourcing spezialisierten Datenbank EventSourcingDB flexibilisiert die Konsistenzsteuerung und erhöht die Sicherheit mit digitalen Signaturen für Events. Event Sourcing ist ein Architekturverfahren, das alle Veränderungen des Zustands einer Anwendung als Sequenz von Events abbildet.

In der neuen Version der EventSourcingDB lassen Konsistenzgrenzen nicht nur über Aggregate festlegen, sondern auch flexibel über EventQL-Abfragen beschreiben. Mit diesen Dynamic Consistency Boundaries (DCBs) definieren Teams genau, wann strikte Konsistenz notwendig ist und wo Eventual Consistency konform zur offiziellen Spezifikation genügt. Dazu implementierte der Freiburger Hersteller the native web eine neue Precondition, die die Datenbank vor dem Schreiben von Events prüft.

Ebenfalls neu sind per Ed25519 signierte Events. Die Datenbank erzeugt die Signaturen on-the-fly, was Schlüsselwechsel ermöglicht, ohne bestehende Events anpassen zu müssen. Client-SDKs unterstützen die Verifikation inklusive Hash-Prüfung in Go und JavaScript, weitere Sprachen sollen folgen. Damit lassen sich Integrität und Authentizität von Events gegenüber Auditoren und Regulatoren zuverlässig nachweisen.

Im Detail gibt es weitere Verbesserungen: Der integrierte EventQL-Editor bietet Autovervollständigung für Event-Typen, neue sowie konsistentere API-Endpunkte erleichtern den Zugriff, und das Management-UI wurde leicht überarbeitet. Zahlreiche Optimierungen im Hintergrund sollen Stabilität und Entwicklerfreundlichkeit erhöhen.


Scrennshot Abfragen

Scrennshot Abfragen

Die Abfragen in der EventSourcingDB erfolgen über die Sprache EventQL.

(Bild: the native web)

Die EventSourcingDB steht als Docker-Image und als Binary für Linux, macOS und Windows bereit. Offizielle SDKs existieren für Go, JavaScript/TypeScript, .NET, PHP, Python, Rust und die JVM. Die Nutzung ist bis 25.000 Events pro Instanz kostenlos. Zudem startet der Anbieter eine gemanagte Version als private Beta. Sie richtet sich an Teams, die die Datenbank nutzen möchten, ohne selbst Infrastruktur betreiben zu müssen.


(who)



Source link

Weiterlesen

Entwicklung & Code

programmier.bar: Cloud-Exit mit Andreas Lehr


Cloud-Exit ist aktuell ein heiß diskutiertes Thema – spätestens seit Firmen wie Basecamp angekündigt haben, die Cloud zu verlassen und wieder auf eigene Infrastruktur zu setzen.

In dieser Podcastfolge sprechen Dennis Becker und Jan Gregor Emge-Triebel mit Andreas Lehr, Berater für Cloud- und DevOps-Themen und Herausgeber des Podcasts „Happy Bootstrapping“ sowie des Newsletters „Alles nur gecloud“. Gemeinsam diskutieren sie, was neben hohen Kosten noch für einen Cloud-Exit spricht: von Vendor-Lock-ins über Datenschutz bis hin zu Performance und digitaler Souveränität. Dabei berichtet Andreas Lehr von realen Erfahrungen aus der Praxis.

Empfohlener redaktioneller Inhalt

Mit Ihrer Zustimmung wird hier ein externer Inhalt geladen.

Einig sind sich die drei, dass Hyperscaler durchaus weiter ihre Berechtigung haben – aber wo genau fängt ein Cloud-Exit an, und wie nah muss ein Unternehmen seinen eigenen Servern wieder kommen, damit sich der Schritt lohnt?

Die aktuelle Ausgabe des Podcasts steht auch im Blog der programmier.bar bereit: „Cloud-Exit mit Andreas Lehr„. Fragen und Anregungen gerne per Mail oder via Mastodon, Bluesky, LinkedIn oder Instagram.


(mai)





Source link

Weiterlesen

Beliebt