Parallele (GPU) Algorithmen für asynchrone zellulare Automaten

12

Ich habe eine Sammlung von Rechenmodellen, die als asynchrone zellulare Automaten beschrieben werden könnten. Diese Modelle ähneln dem Ising-Modell, sind jedoch etwas komplizierter. Es scheint, als würden solche Modelle davon profitieren, auf einer GPU anstatt auf einer CPU betrieben zu werden. Leider ist es nicht ganz einfach, ein solches Modell zu parallelisieren, und mir ist überhaupt nicht klar, wie ich damit umgehen soll. Mir ist bewusst, dass es Literatur zu diesem Thema gibt, aber alles scheint sich an Hardcore-Informatiker zu richten, die sich für Details der algorithmischen Komplexität interessieren, und nicht an jemanden wie mich, der nur eine Beschreibung von etwas möchte, das ich implementieren kann, und folglich finde ich es eher undurchdringlich.

Der Klarheit halber suche ich nicht so sehr nach einem optimalen Algorithmus, als nach etwas, das ich schnell in CUDA implementieren kann und das wahrscheinlich zu einer deutlichen Beschleunigung meiner CPU-Implementierung führt. Die Programmierzeit ist in diesem Projekt ein wesentlich begrenzenderer Faktor als die Computerzeit.

Ich sollte auch klarstellen, dass ein asynchroner zellularer Automat etwas anderes ist als ein synchroner, und dass Techniken zur Parallelisierung synchroner CAs (wie Conways Leben) nicht einfach an dieses Problem angepasst werden können. Der Unterschied besteht darin, dass eine synchrone Zertifizierungsstelle jede Zelle zu jedem Zeitschritt gleichzeitig aktualisiert, während eine asynchrone Zertifizierungsstelle zu jedem Zeitschritt eine zufällig ausgewählte lokale Region aktualisiert, wie unten dargestellt.

Die Modelle, die ich parallelisieren möchte, sind auf einem Gitter (normalerweise einem hexagonalen) implementiert, das aus ~ 100000 Zellen besteht (obwohl ich gerne mehr verwenden würde), und der nicht parallelisierte Algorithmus für deren Ausführung sieht folgendermaßen aus:

  1. Wählen Sie ein benachbartes Zellenpaar nach dem Zufallsprinzip

  2. Berechnen Sie eine "Energie" -Funktion basierend auf einer diese Zellen umgebenden lokalen NachbarschaftΔE

  3. Vertauschen Sie mit einer Wahrscheinlichkeit, die von abhängt (mit a parameter), entweder die Zustände der beiden Zellen oder tun Sie nichts.e-βΔEβ

  4. Wiederholen Sie die obigen Schritte auf unbestimmte Zeit.

Es gibt auch einige Komplikationen im Zusammenhang mit den Randbedingungen, aber ich kann mir vorstellen, dass diese für die Parallelisierung keine großen Schwierigkeiten bereiten.

Es ist erwähnenswert, dass ich mich eher für die transiente Dynamik dieser Systeme als nur für den Gleichgewichtszustand interessiere. Deshalb brauche ich etwas, das der obigen Dynamik entspricht, und nicht nur etwas, das sich der gleichen Gleichgewichtsverteilung annähert. (Daher sind Variationen des Schachbrett-Algorithmus nicht das, wonach ich suche.)

Die Hauptschwierigkeit bei der Parallelisierung des obigen Algorithmus sind Kollisionen. Da alle Berechnungen nur von einer lokalen Region des Gitters abhängen, können viele Gitterstandorte parallel aktualisiert werden, sofern sich ihre Nachbarschaften nicht überlappen. Die Frage ist, wie solche Überschneidungen vermieden werden können. Ich kann mir verschiedene Möglichkeiten vorstellen, aber ich weiß nicht, welche, wenn überhaupt, die beste ist, die ich implementieren kann. Dies sind wie folgt:

  • Verwenden Sie die CPU, um eine Liste von zufälligen Gitterstandorten zu erstellen und auf Kollisionen zu prüfen. Wenn die Anzahl der Grid-Standorte der Anzahl der GPU-Prozessoren entspricht oder wenn eine Kollision festgestellt wird, senden Sie jeden Koordinatensatz an eine GPU-Einheit, um den entsprechenden Grid-Standort zu aktualisieren. Dies wäre einfach zu implementieren, würde aber wahrscheinlich keine große Beschleunigung bringen, da das Überprüfen auf Kollisionen auf der CPU wahrscheinlich nicht viel billiger wäre, als das gesamte Update auf der CPU durchzuführen.

  • Teilen Sie das Gitter in Regionen auf (eine pro GPU-Einheit) und lassen Sie eine GPU-Einheit für die zufällige Auswahl und Aktualisierung von Gitterzellen in ihrer Region verantwortlich sein. Aber es gibt viele Probleme mit dieser Idee, die ich nicht lösen kann. Das offensichtlichste ist, was genau passieren sollte, wenn eine Einheit eine Nachbarschaft auswählt, die den Rand ihrer Region überlappt.

  • Annähern Sie das System wie folgt: Lassen Sie die Zeit in diskreten Schritten ablaufen. Teilen Sie das Gitter in ein anderes aufSatz von Regionen in jedem Zeitschritt gemäß einem vordefinierten Schema, und jede GPU-Einheit wählt zufällig ein Paar von Gitterzellen aus und aktualisiert sie, deren Nachbarschaft die Grenzen der Region nicht überlappt. Da sich die Grenzen bei jedem Schritt ändern, wirkt sich diese Einschränkung möglicherweise nicht zu stark auf die Dynamik aus, solange die Bereiche relativ groß sind. Dies scheint einfach zu implementieren und wahrscheinlich schnell zu sein, aber ich weiß nicht, wie gut es die Dynamik annähert, oder was das beste Schema für die Auswahl der Regionsgrenzen bei jedem Zeitschritt ist. Ich habe einige Verweise auf "block-synchrone zellulare Automaten" gefunden, die möglicherweise mit dieser Idee identisch sind oder nicht. (Ich weiß es nicht, weil es den Anschein hat, dass alle Beschreibungen der Methode entweder in Russisch sind oder sich in Quellen befinden, auf die ich keinen Zugriff habe.)

Meine spezifischen Fragen lauten wie folgt:

  • Sind die oben genannten Algorithmen eine sinnvolle Methode, um die GPU-Parallelisierung eines asynchronen CA-Modells zu erreichen?

  • Gibt es einen besseren Weg?

  • Gibt es einen Bibliothekscode für diese Art von Problem?

  • Wo finde ich eine eindeutige Beschreibung der block-synchronen Methode in englischer Sprache?

Fortschritt

Ich glaube, ich habe eine Möglichkeit gefunden, eine möglicherweise geeignete asynchrone Zertifizierungsstelle zu parallelisieren. Der im Folgenden beschriebene Algorithmus gilt für eine normale asynchrone Zertifizierungsstelle, die jeweils nur eine Zelle aktualisiert, und nicht für ein benachbartes Zellenpaar wie meins. Es gibt einige Probleme bei der Verallgemeinerung auf meinen speziellen Fall, aber ich denke, ich habe eine Idee, wie ich sie lösen kann. Ich bin mir jedoch nicht sicher, wie viel Geschwindigkeitsvorteil sich daraus ergibt, und zwar aus den nachfolgend erläuterten Gründen.

Die Idee ist, die asynchrone Zertifizierungsstelle (fortan ACA) durch eine stochastische synchrone Zertifizierungsstelle (SCA) zu ersetzen, die sich gleichwertig verhält. Dazu stellen wir uns zunächst vor, dass der ACA ein Poisson-Prozess ist. Das heißt, die Zeit läuft kontinuierlich ab, und jede Zelle hat eine konstante Wahrscheinlichkeit pro Zeiteinheit, ihre Aktualisierungsfunktion unabhängig von den anderen Zellen auszuführen.

Xichjtichjtichj(0)Exp(λ)λ ist ein Parameter, dessen Wert beliebig gewählt werden kann.)

Bei jedem logischen Zeitschritt werden die Zellen der SCA wie folgt aktualisiert:

  • k,lich,jtkl<tichj

  • XichjXklΔtExp(λ)tichjtichj+Δt

Ich glaube, dies garantiert, dass die Zellen in einer Reihenfolge aktualisiert werden, die "decodiert" werden kann, um der ursprünglichen ACA zu entsprechen, während Kollisionen vermieden werden und ermöglicht wird, dass einige Zellen parallel aktualisiert werden. Aufgrund des ersten Aufzählungspunkts oben bedeutet dies jedoch, dass die meisten GPU-Prozessoren in jedem Zeitschritt des SCA größtenteils im Leerlauf sind, was weniger als ideal ist.

Ich muss etwas mehr darüber nachdenken, ob die Leistung dieses Algorithmus verbessert werden kann und wie dieser Algorithmus erweitert werden kann, um mit dem Fall umzugehen, dass mehrere Zellen gleichzeitig im ACA aktualisiert werden. Da es jedoch vielversprechend aussieht, dachte ich, ich würde es hier beschreiben, falls jemand (a) etwas Ähnliches in der Literatur kennt oder (b) einen Einblick in diese verbleibenden Probleme geben kann.

Nathaniel
quelle
Vielleicht können Sie Ihr Problem in einem auf Schablonen basierenden Ansatz formulieren. Es gibt viel Software für Probleme mit Schablonen. Sie können einen Blick darauf werfen: libgeodecomp.org/gallery.html , Conways Spiel des Lebens. Dies könnte einige Ähnlichkeiten haben.
vanCompute
@vanCompute, das wie ein fantastisches Tool aussieht, aber meiner ersten (eher flüchtigen) Untersuchung zufolge scheint das Schablonencode-Paradigma von Natur aus synchron zu sein, sodass es wahrscheinlich nicht gut zu dem passt, was ich versuche. Ich werde mich jedoch weiter damit befassen.
Nathaniel
Können Sie ein paar Details darüber angeben, wie Sie dies mit SIMT parallelisieren würden? Würden Sie einen Faden pro Paar verwenden? Oder kann die Arbeit, die mit dem Aktualisieren eines einzelnen Paares verbunden ist, auf 32 oder mehr Threads verteilt werden?
Pedro
@Pedro Die Arbeit bei der Aktualisierung eines einzelnen Paares ist relativ gering (im Grunde genommen nur eine Zusammenfassung der Nachbarschaft plus einer Iteration eines Zufallszahlengenerators und einer exp()), daher hätte ich nicht gedacht, dass es viel Sinn macht, es auf mehrere Threads zu verteilen. Ich denke, es ist besser (und für mich einfacher), mehrere Paare parallel zu aktualisieren, mit einem Paar pro Thread.
Nathaniel
Ok, und wie definieren Sie eine Überlappung zwischen Updates zu koppeln? Überlappen sich die Paare oder überlappen sich ihre Nachbarschaften?
Pedro

Antworten:

4

Ich würde die erste Option verwenden und würde einen synchronen AC-Lauf verwenden, bevor (unter Verwendung der GPU), um Kollisionen zu erkennen, einen Schritt eines hexagonalen AC ausführen, dessen Regel der Wert der mittleren Zelle ist = Summe (Nachbarn), die diese CA haben muss Sieben Status sollten mit einer zufällig ausgewählten Zelle initiiert und ihr Status überprüft werden, bevor die Aktualisierungsregel für jede GPU ausgeführt wird.

Beispiel 1. Der Wert einer benachbarten Zelle wird geteilt

0 0 0 0 0 0 0 0

  0 0 1 0 0 0

0 0 0 0 0 0 0 0

  0 0 0 1 0 0

0 0 0 0 0 0 0 0

ein Schritt einer CA, deren Regel hexagonale zentrale Zelle ist = Summe (Nachbarn)

0 0 1 1 0 0 0

  0 1 1 1 0 0

0 0 1 2 1 0 0

  0 0 1 1 1 0

0 0 0 1 1 0 0

Beispiel 2. Der Wert einer zu aktualisierenden Zelle wird als Nachbarn auf der anderen Seite berücksichtigt

0 0 0 0 0 0 0 0

  0 0 1 0 0 0

0 0 0 1 0 0 0

  0 0 0 0 0 0

0 0 0 0 0 0 0 0

Nach der Iteration

0 0 1 1 0 0 0

  0 1 2 2 0 0

0 0 2 2 1 0 0

  0 0 1 1 0 0

0 0 0 0 0 0 0 0

Probe 3. Es besteht keine Beziehung

  0 0 0 0 0 0

0 0 1 0 0 0 0

  0 0 0 0 0 0

0 0 0 0 0 0 0 0

  0 0 0 1 0 0

0 0 0 0 0 0 0 0

Nach der Iteration

  0 1 1 0 0 0

0 1 1 1 0 0 0

  0 1 1 0 0 0

0 0 0 1 1 0 0

  0 0 1 1 1 0

0 0 0 1 1 0 0

jlopez1967
quelle
Ö(n)n
Ich denke, es gibt viel, was parallelisiert werden kann. Die Kollisionsverarbeitung wird vollständig auf der GPU ausgeführt. Dies ist ein Schritt in einem synchronen Wechselstrom, wie in der oben angegebenen Verknüpfung gezeigt. Für die Überprüfung wird eine lokale Regel verwendet, wenn Summe (Nachbarn) = 8 KEINE Kollision, Summe (Nachbarn)> 8 Kollision. Sie wird überprüft, bevor die Änderung der Aktualisierungsregel ausgeführt wird, wenn keine Kollisionszellenzustände vorliegen, da die beiden nahe beieinander platziert werden sollten Die Punkte, die bewertet werden müssen, wenn sie nicht nahe beieinander liegen, gehören zu anderen Zellen.
jlopez1967
Ich verstehe das, aber das Problem ist, was machst du, wenn du eine Kollision feststellst? Wie oben erläutert, ist Ihr CA-Algorithmus nur der erste Schritt zur Erkennung einer Kollision. Der zweite Schritt besteht darin, das Raster nach Zellen mit einem Zustand> = 2 zu durchsuchen, und dies ist nicht trivial.
Nathaniel
Beispiel: Stellen Sie sich vor, wir möchten die Kollisionszelle (5.7) auf zellularen Automaten und der ausgeführten Summe (Nachbarn der Zelle (5,7)) erkennen, und wenn der Wert 8 ist und wenn keine Kollision größer als 8 ist, ist dies keine Kollision sollte in der Funktion sein, die jede Zelle auswertet, um den nächsten Zustand der Zelle in asynchronen zellularen Automaten zu definieren. Die Erkennung von Kollisionen für jede Zelle ist eine lokale Regel, an der nur die benachbarten Zellen beteiligt sind
jlopez1967
Ja, aber die Frage, die wir beantworten müssen, um eine asynchrone Zertifizierungsstelle parallelisieren zu können, lautet nicht "Gab es eine Kollision in Zelle (5,7)", sondern "Gab es eine Kollision irgendwo im Raster, und wenn ja, wo?" es?" Das kann nicht beantwortet werden, ohne über das Raster zu iterieren.
Nathaniel
1

Nach Ihren Antworten auf meine Fragen in den obigen Kommentaren würde ich vorschlagen, dass Sie einen sperrenbasierten Ansatz verwenden, bei dem jeder Thread versucht, die zu aktualisierende Umgebung zu sperren, bevor das tatsächliche Update berechnet wird.

Sie können dies mit den in CUDA vorgesehenen atomaren Operationen und einem Array intmit den Sperren für jede Zelle tun , z lock. Jeder Thread führt dann Folgendes aus:

ci, cj = choose a pair at random.

int locked = 0;

/* Try to lock the cell ci. */
if ( atomicCAS( &lock[ci] , 0 , 1 ) == 0 ) {

    /* Try to lock the cell cj. */
    if ( atomicCAS( &lock[cj] , 0 , 1 ) == 0 ) {

        /* Now try to lock all the neigbourhood cells. */
        for ( cn = indices of all neighbours )
            if ( atomicCAS( &lock[cn] , 0 , 1 ) != 0 )
                break;

        /* If we hit a break above, we have to unroll all the locks. */
        if ( cn < number of neighbours ) {
            lock[ci] = 0;
            lock[cj] = 0;
            for ( int i = 0 ; i < cn ; i++ )
                lock[i] = 0;
            }

        /* Otherwise, we've successfully locked-down the neighbourhood. */
        else
            locked = 1;

        }

    /* Otherwise, back off. */
    else
        lock[ci] = 0;
    }

/* If we got everything locked-down... */
if ( locked ) {

    do whatever needs to be done...

    /* Release all the locks. */
    lock[ci] = 0;
    lock[cj] = 0;
    for ( int i = 0 ; i < cn ; i++ )
        lock[i] = 0;

    }

Beachten Sie, dass dieser Ansatz wahrscheinlich nicht der optimalste ist, aber einen interessanten Ausgangspunkt bieten könnte. Wenn es viele Kollisionen zwischen Threads gibt, dh eine oder mehrere pro 32 Threads (wie bei einer Kollision pro Warp), wird es ein gutes Stück Verzweigungsumleitung geben. Atomare Operationen können auch etwas langsam sein, aber da Sie nur Compare-and-Swap-Operationen ausführen, sollte die Skalierung in Ordnung sein.

Der Aufwand für das Sperren mag einschüchternd aussehen, aber es sind wirklich nur ein paar Aufgaben und Zweige, nicht viel mehr.

Beachten Sie auch, dass ich schnell und locker mit Notation in den Schleifen ider Nachbarn bin .

Nachtrag: Ich war unbekümmert genug anzunehmen, dass man sich einfach zurückziehen könnte, wenn Paare kollidieren. Ist dies nicht der Fall, können Sie ab der zweiten Zeile alles in eine while-Loop einschließen und breakam Ende der finalen if-Anweisung ein hinzufügen.

Alle Threads müssen dann warten, bis der letzte erledigt ist, aber wenn Kollisionen selten sind, sollten Sie in der Lage sein, damit durchzukommen.

Anhang 2: Versuchen Sie nicht , __syncthreads()irgendwo in diesem Code Anrufe hinzuzufügen , insbesondere in der im vorherigen Anhang beschriebenen Loop-Version! Eine Asynchronität ist wesentlich, um im letzteren Fall wiederholte Kollisionen zu vermeiden.

Pedro
quelle
Danke, das sieht ziemlich gut aus. Wahrscheinlich besser als die komplizierte Idee, über die ich nachgedacht habe, und viel einfacher umzusetzen. Ich kann Kollisionen seltener machen, indem ich ein ausreichend großes Gitter verwende, was wahrscheinlich in Ordnung ist. Wenn sich herausstellt, dass die Just-Back-Off-Methode erheblich schneller ist, kann ich sie zur informellen Untersuchung der Parameter verwenden und auf die Methode "Warten auf alle anderen zur Vervollständigung" umschalten, wenn ich offizielle Ergebnisse erzielen muss. Ich werde es bald versuchen.
Nathaniel
1

Ich bin der Hauptentwickler von LibGeoDecomp. Ich bin mit vanCompute einverstanden, dass Sie Ihren ACA mit einer Zertifizierungsstelle emulieren können, aber Sie haben Recht, dass dies nicht sehr effizient ist, da nur wenige Zellen in einem bestimmten Schritt aktualisiert werden sollen. Dies ist in der Tat eine sehr interessante Anwendung - und es macht Spaß, daran zu basteln!

Ich empfehle Ihnen, die von jlopez1967 und Pedro vorgeschlagenen Lösungen zu kombinieren: Der Algorithmus von Pedro erfasst die Parallelität gut, aber diese atomaren Verriegelungen sind schrecklich langsam. Die Lösung von jlopez1967 ist elegant, wenn es darum geht, Kollisionen zu erkennen, aber alle nZellen zu überprüfen , wenn nur eine kleinere Teilmenge (von nun an gehe ich davon aus, dass es einen Parameter gibt, kder die Anzahl der gleichzeitig zu aktualisierenden Zellen angibt) aktiv ist. ist eindeutig unerschwinglich.

__global__ void markPoints(Cell *grid, int gridWidth, int *posX, int *posY)
{
    int id = blockIdx.x * blockDim.x + threadIdx.x;
    int x, y;
    generateRandomCoord(&x, &y);
    posX[id] = x;
    posY[id] = y;
    grid[y * gridWidth + x].flag = 1;
}

__global__ void checkPoints(Cell *grid, int gridWidth, int *posX, int *posY, bool *active)
{
    int id = blockIdx.x * blockDim.x + threadIdx.x;
    int x = posX[id];
    int y = posY[id];
    int markedNeighbors = 
        grid[(y - 1) * gridWidth + x + 0].flag +
        grid[(y - 1) * gridWidth + x + 1].flag +
        grid[(y + 0) * gridWidth + x - 1].flag +
        grid[(y + 0) * gridWidth + x + 1].flag +
        grid[(y + 1) * gridWidth + x + 0].flag +
        grid[(y + 1) * gridWidth + x + 1].flag;
    active[id] = (markedNeighbors > 0);
}


__global__ void update(Cell *grid, int gridWidth, int *posX, int *posY, bool *active)
{
    int id = blockIdx.x * blockDim.x + threadIdx.x;
    int x = posX[id];
    int y = posY[id];
    grid[y * gridWidth + x].flag = 0;
    if (active[id]) {
        // do your fancy stuff here
    }
}

int main() 
{
  // alloc grid here, update up to k cells simultaneously
  int n = 1024 * 1024;
  int k = 1234;
  for (;;) {
      markPoints<<<gridDim,blockDim>>>(grid, gridWidth, posX, posY);
      checkPoints<<<gridDim,blockDim>>>(grid, gridWidth, posX, posY, active);
      update<<<gridDim,blockDim>>>(grid, gridWidth, posX, posY, active);
  }
}

In Ermangelung einer guten globalen Synchronisation auf der GPU müssen Sie mehrere Kernel für die verschiedenen Phasen aufrufen. Auf Nvidias Kepler können Sie sogar die Hauptschleife zur GPU verschieben, aber ich erwarte, dass das nicht viel bringt.

Die Algorithmen erreichen einen (konfigurierbaren) Parallelitätsgrad. Ich denke, die interessante Frage ist, ob die Kollisionen Ihre zufällige Verteilung beeinflussen, wenn Sie zunehmen k.

Gentryx
quelle
0

Ich schlage Ihnen vor, dass Sie diesen Link http://www.wolfram.com/training/courses/hpc021.html ungefähr 14:15 Minuten in das Video natürlich Mathematica-Training sehen, wo sie eine Implementierung eines zellularen Automaten mit CUDA durchführen , von dort und Sie können es ändern.

Juan Carlos Lopez
quelle
Leider handelt es sich hierbei um eine synchrone Zertifizierungsstelle, die eine andere Art von Bestie ist als die asynchronen, mit denen ich es zu tun habe. In einer synchronen Zertifizierungsstelle wird jede Zelle gleichzeitig aktualisiert, und dies ist auf einer GPU leicht zu parallelisieren, aber in einer asynchronen Zertifizierungsstelle wird bei jedem Zeitschritt eine einzelne zufällig ausgewählte Zelle aktualisiert (in meinem Fall sind es zwei benachbarte Zellen) die Parallelisierung viel schwieriger. Die in meiner Frage beschriebenen Probleme beziehen sich speziell auf das Erfordernis einer asynchronen Aktualisierungsfunktion.
Nathaniel