Forum: FPGA, VHDL & Co. Pipeline MAC-Unit: Problem mit Latency


von Jens W. (jensw)


Angehängte Dateien:

Lesenswert?

Hallo zusammen,

ich bräuchte eure Hilfe, ich komme gerade nicht weiter.
Ich spiele wieder mit meinem Evalboard herum.
Ich möchte eine MAC Einheit bauen um ein Skalarprodukt zu berechnen. Die 
Werte für die Multiplikation kommen von Input und Konstanten aus einem 
RAM. Die Eingangswerte korrekt einspeisen funktioniert richtig.

Ich verwende einen Multiplizierer und einen Addierer. Beide Cores habe 
(zugunsten höherer Durchgangsgeschwindigkeit) eine Latency von 6 Takten.
Ich habe den Multiplizierer und den Addierer hintereinander geschaltet. 
Ich versuche die Latency zu warten, bevor ich das Ergebnis an den 
Ausgang übergebe, aber durch die zeitlich verschobenen Ergebnisse ist 
das Resultat am Ausgang nicht richtig. (Richtig, das ist mein erster 
Versuch für eine Pipeline Struktur)

Die Anwendung sollte mit der Beschreibung auch klar sein, das ist für 
ein neuronales Netz.
Ich hänge euch meine Lösung mit an.
Meine Frage ist nun, wie muss ich die Pipeline für den Multiplizierer 
und den Addierer richtig machen?

Grüße, Jens

von Rolf (audiorolf)


Lesenswert?

Was ich sehe ist, dass das ready nicht benutzt wird. Du timest also 
händisch.
Das passt aber nicht nur funktionell- sondern auch technisch nicht, weil 
das keine echte pipeline ist. Irgendwie hast du da noch eine FSM drüber 
laufen die auch getaktest ist und es sieht fuer mich so aus, als ob 
damit timing -Anforderungen entstehen, d.h. es wird nach dem erwarteten 
Ende der Berechnung sofort ein Ergebnis weitergeschrieben, das noch 
nicht da ist.

So ganz werde ich aus dem Konstrukt eh nicht schlau. Eigentlich schreibt 
man einfach  ein Y = A * B und setzt genügend FFs dahinter, dass es 
technisch passt. Das Gemurkse mit Adder-Cores ist an der Stelle 
eigentlich unnötigt.

von Jens W. (jensw)


Lesenswert?

Hallo Rolf,

danke für deine Antwort.
Allerdings ist es nicht möglich einfach zu schreiben: Y = A * B. Es 
müsste heißen Y <= A * B, aber das wird trotzdem nicht funktionieren. 
Wenn du genau schaust, verwende ich die Cores, da die Berechnungen in 
Float16 laufen.
In einer Signed-Arithmetik würde ich das eh ganz anders machen.

Die FSM brauche ich, da hier die Multiplexer versteckt sind, die die 
Daten zu den richtigen Cores schieben. Durch die Latenz kann ich das 
nicht durchverdrahten, sonst kommt nur Murks raus. Außerdem fehlt noch 
die Aktivierungsfunktion und da will ich die beiden Cores nochmal 
verwenden.

Aber wenn die Pipeline bei mir nicht richtig ist, wie geht es denn 
richtig?

Grüße, Jens

von Rick D. (rickdangerus)


Lesenswert?

1
Component instance "/Neuron/My_Weight_Memory : Weight_Memory" is not bound.
2
Component instance "/Neuron/My_Float_Multiply : Float_Multiply" is not bound.
3
Component instance "/Neuron/My_Float_Addition : Float_Addition" is not bound.
Wäre es möglich die fehlenden Komponenten nachzureichen? Dann könnte ich 
das in der Simulation evtl. nachvollziehen...

von Jens W. (jensw)


Angehängte Dateien:

Lesenswert?

Hallo Rick,

klar, die kann ich auch bereitstellen.
Allerdings sind Float_Multiply und Float_Addition Cores von Xilinx. Was 
brauchst du da? Da habe ich ja keine Sourcen.
Ich hänge dir den Ordner, den Core Generator anlegt, an.
Und ich lege dir die Testbench mit dazu, die ich verwende. Dann hast du 
für die Simulation ein paar Werte.

Zu Weight_Memory:
Das ist als Dual-Port Ram gemacht, dass man später von außen an die 
Werte kommt. Der zweite Port ist noch nicht verbunden. Aber für das 
Problem hier, ist das kein Thema.

Danke dir!

Grüße, Jens

von Martin S. (strubi)


Lesenswert?

Für eine Pipeline brauchst du eigentlich nur ein durchgereichtes 
Enable-Signal, damit hakt man dann auch allfällige 
Start/Stop-Bedingungen ab. Wenn du gemultiplext mehrere Datenströme 
quasi-gleichzeitig durch die Pipe schieben willst, ist eine FSM eher 
fehl am Platz, da für die einzelnen Stufen ein Zeitversatz des Zustands 
gelten muss, besser ist ein Slot/Lane-Konzept mit rotierenden Indizes 
per Pipeline-Stufe. Den Slot-Index nimmst du dann auch für die 
Adressierung von Koeffizienten in der entsprechenden Stufe.

Wenn du an die Sourcen nicht rankommst: Es kann auch fürs Lernen helfen, 
händisch Dummies zu schreiben (zu lassen), die intern die laufenden 
Indizes und Latenzen mitführen. Dann kannst du für die finale 
Verifikation immer noch die erzeugten Netzlisten per "netgen" in HDL 
umwandeln und gegen dein Dummy co-verifizieren (also feststellen, dass 
es richtig rechnet).

Vordergründig springt mir zumindest bei deinem Code keine 
Pipeline-Struktur ins Auge. Ich würde mir da eine andere Notation 
überlegen und gerade bei extern eingeschleiften Cores per Kommentar 
kennzeichnen, wann ein Resultat gültig ist. Per explizite 
Ausformulierung oder Skizzierung einer Pipeline auf Häuschenpapier 
siehst du auch sofort, wo Delays nötig sind und man allenfalls die 
Resourcen besser nutzen kann.

: Bearbeitet durch User
von Rick D. (rickdangerus)


Angehängte Dateien:

Lesenswert?

Jens W. schrieb:
> klar, die kann ich auch bereitstellen.
Danke.

> Allerdings sind Float_Multiply und Float_Addition Cores von Xilinx. Was
> brauchst du da? Da habe ich ja keine Sourcen.
Da sind doch Sourcen dabei (z.B. Float_Multipy.vhd). Das sind aber nur 
Wrapper für die XilinxCoreLib. Die ist bei mir vorhanden.

Mich wundert nur die vhd-Dateien aus ipcore_dir.rar haben kein 
ce-Signal, aber in Neuron.vhd wird es mit angesteuert.

Außerdem bemängelt Modelsim noch ein paar Kleinigkeiten:
1
-- Compiling architecture behavior of Neuron_tb
2
** Warning: Neuron_tb.vhd(170): (vcom-1207) An abstract literal and an identifier must have a separator between them.
3
** Warning: Neuron_tb.vhd(172): (vcom-1207) An abstract literal and an identifier must have a separator between them.
4
** Warning: Neuron_tb.vhd(176): (vcom-1207) An abstract literal and an identifier must have a separator between them.
5
-- Compiling entity Weight_Memory
6
-- Compiling architecture Behavioral of Weight_Memory
7
** Warning: Weight_Memory.vhd(142): (vcom-1074) Non-locally static OTHERS choice is allowed only if it is the only 
8
choice of the only association.

Ansonsten ist nach 350 ns Schluß und die Simulation crasht mit:
1
# ** Fatal: (vsim-3421) Value 10 is out of range 0 to 9.
2
#    Time: 357500 ps  Iteration: 1  Process: /neuron_tb/uut/line__157 File: Neuron.vhd Line: 186

Dann würde ich int_execute in der Testbench noch initialisiern:
1
  signal int_execute : std_logic := '0';
Damit sieht das ready-Signal etwas besser aus.

Wie und wo kann ich in der Simulation Dein Latency-Problem finden?

: Bearbeitet durch User
von Jens W. (jensw)


Angehängte Dateien:

Lesenswert?

Hallo Rick,

danke, dass du dir die Mühe machst.
Ich habe dir nochmal das aktuelle File angehängt. Da ist das CE-Signal 
nicht mehr drin. Ich probiere natürlich weiter und als ich die Cores 
zusammengepackt habe, war ich schon weiter, als mein Post vom Anfang. 
Sorry, dafür, das habe ich nicht gemerkt.

Ich habe dir noch ein Bild aus meiner Simulation mit angehängt und da 
kann man das Problem gut erkennen.
Rot unterstrichen ist die Latency aus dem Mult-Core. Man kann gut 
erkennen, dass das erste Ergebnis 6 Takte später geliefert wird. Für die 
Multiplikation ist das kein Problem, das funktioniert.
Blau unterstrichen ist die Latency aus dem Addierer und da ist das 
Problem.
Da das ein Akkumultor sein soll, wird das Ergebnis an den Eingang 
zurückgeführt. Dabei sollen die Eingangswerte mit dem Ergebnis der 
Multiplikation jeweils aufaddiert werden. Da die ersten Ergebnisse des 
Addierers aber erst 6 Takte später zurück kommen (Durchlauf durch den 
Addierer), werden die ersten 6 Eingangswerte jeweils mit Null addiert. 
Somit ist das Ergebnis später falsch. Es sind zu wenig Werte addiert 
(gelber Kasten).

Man muss wohl die ersten 6 Eingangswerte in einem kleinen lokalen 
Speicher festhalten und sie am Ende nochmal auf den Addierer geben. Die 
Reihenfolge der Werte, die addiert werden sollen, spielt ja keine Rolle. 
Hauptsache jedes Signal wird addiert.

Im aktuellen File ist der Buffer dafür schon angelegt, jedoch bin ich 
noch am Anfang, dass das Timing passt.
Ich möchte das nicht für einen Fall (ein Wert für die Latency) fest 
codieren, da später die Cores vielleicht angepasst werden und dann mehr 
oder weniger Latency haben, oder ich mehr Eingänge verwende (und das 
werde ich), dann möchte ich nicht mehr am Timing fummeln müssen.

Ich hoffe das Problem ist damit nochmal deutlicher beschrieben.

Grüße, Jens

von Rick D. (rickdangerus)


Lesenswert?

Ich komme mit der Simulation gar nicht so weit wie Du:
1
entity Neuron is
2
  generic 
3
  (
4
    ...
5
    INPUTS        : integer := 10;      --Number of used inputs
6
    ...
7
  );
8
9
architecture Behavioral of Neuron is
10
11
  ... 
12
  signal counter      : integer range 0 to INPUTS-1;
13
  ...
14
15
        when load =>
16
          ...
17
          counter <= counter + 1;
18
          if(counter = INPUTS) then
19
            counter      <= 0;
20
            ...
21
          end if;
Warum darf counter nur Werte von 0 bis INPUTS-1 annehmen, wird aber auf 
INPUTS abgefragt?
Und war fällt das in deinem Simulator nicht auf?
Abgesehen davon wird die Endbedingung nicht funktionieren, da 
Signalzuweisungen erst am Ende vom Prozess (und damit hier im nächsten 
Takt) wirksam werden.

von Rick D. (rickdangerus)


Angehängte Dateien:

Lesenswert?

Was ich gerne bei solchen Berechnungen mache: ein enable bzw. 
valid-Signal mit an die Datenleitungen geben. Da kann man
1. in der Simulation sehen, wann die Werte gültig sind und viel 
wichtiger
2. die nachfolgende Stufe kann dieses Signal verwenden um die eigene 
Berechnung zu starten

Hier scheint es ja für die Multiplikation an sich zu funktionieren.
Wenn man bei der Addition aufintegrieren möchte, muß man immer erst fünf 
Takte auf das Ergebnis warten bevor man die nächste Addition starten 
kann.

Im FPGA laufen die Addition i.d.R. in einem Takt, aber das gilt für 
Ganzzahlen und nicht für fp-Zahlen.  Wenn möglich würde ich intern nur 
mit Ganzzahlen rechnen und floating-points nur an den Schnittstellen 
verwenden.

von Jens W. (jensw)


Lesenswert?

Hallo Rick,

da hast du Recht, das ist natürlich falsch. Und wird nicht 
funktionieren. Warum mein Simulationstool hier deutlich toleranter ist 
als deines, ist auch sehr interessant. Ich verwende noch ISE14.7 für den 
alten Kram und den eingebauten Simulator in ISE.

Aber am eigentlichen Problem ändert das nichts. Ist zwar falsch, aber 
mir geht es ja um die Latenz der Cores.

In meinem Ansatz ist alles in einen Prozess gequetscht und das macht es 
schon unübersichtlich. Ich habe nun angefangen das ein bisschen 
aufzuteilen.
Einen Prozess als reiner Counter. Der zählt die Eingänge und die 
Latenzzeiten durch. Den Timer will ich nun dafür nehmen um die anderen 
darauf zu synchronisieren. Wie ein enable-Signal im Prinzip.

Dann habe ich jeweils einen Prozess für Multiplikation und Addition. Und 
das funktioniert auch ganz gut. Und der Ansatz, den ich oben schon 
anpeilte, dass man die ersten Ergebnisse in einem Speicher 
zwischenzuspeichern, scheint auch das Richtige zu sein.
Zusätzlich habe ich die Multiplexer für die Dateneingänge des Addierers 
auch rausgezogen in eine nebenläufige Select-Anweisung. Das funktioniert 
besser als im Prozess selbst. Laut Simulation spart man sich hier sogar 
noch einen Takt.

Was mir jetzt noch fehlt ist die zwischengespeicherten Signale noch auf 
den Addierer zu geben, so dass das Ergebnis wieder stimmt.

Ich lade später noch das Source-File dazu hoch, ich bin gerade an einem 
anderen Rechner.

Grüße, Jens

von Jens W. (jensw)


Angehängte Dateien:

Lesenswert?

Rick D. schrieb:
> Wenn man bei der Addition aufintegrieren möchte, muß man immer erst fünf
> Takte auf das Ergebnis warten bevor man die nächste Addition starten
> kann.

Das stimmt nicht ganz. Man muss bei den Cores, die ich verwende, nicht 
auf jedes Ergebnis 5 Takte warten. Die Cores haben eine Pipeline. Man 
bekommt nach der Latenz (hier 6 Takte) das erste Ergebnis und dann mit 
jedem Takt das nächste. Daher verwende ich die auch, da man so den 
großen Datendurchsatz bekommt.

So sieht es im Moment aus (Anhang). Es ist noch nicht fertig. Durch den 
Multiplizierer laufen die Daten sauber durch. Die ersten Ergebnisse 
werden auch im Buffer richtig abgelegt. Das Finish fehlt noch, wo die 
Werte aus dem Buffer nochmal angehängt werden.

Die Signale für die Select-Anweisung sind als Vektoren. Die könnte man 
kürzen, aber ich weiß noch nicht was ich noch brauche. Kürzen kann ich 
später, wenn das Timing insgesamt passt.

Den Weg, den ich einschlagen will, ist klar. Aber ist das auch der 
effektivste? Oder übersehe ich was und man kann das ganz elegant machen?
Wichtig ist nur: ich möchte nicht von den IP-Cores weggehen. Also ein 
Umbau auf Integer kommt für mich nicht in Frage. Ich möchte die 
Berechnungen unbedingt in Float16 behalten.

Grüße, Jens

von Rick D. (rickdangerus)


Lesenswert?

Jens W. schrieb:
> Die Cores haben eine Pipeline. Man
> bekommt nach der Latenz (hier 6 Takte) das erste Ergebnis und dann mit
> jedem Takt das nächste.
Richtig. Wenn Du aber die Ergebnisse der Multiplikation aufsummieren 
möchtest, solltest Du immer erst auf die Zwischensumme warten, bevor der 
nächste Wert addiert werden kann.

von Jens W. (jensw)



Lesenswert?

Hallo Rick,

ich bin einen Schritt weiter, aber der wirft mich leider weit zurück!

Also einfach mit einem Buffer und die Werte nochmal oben drauf und gut 
ist es, so wird das nichts. Man bekommt ein Ergebnis, das zwar einzelne 
Eingänge miteinander addiert, aber die echte Summe über alle Werte 
bekommt man nicht. Dafür müsste man mehrere Durchläufe machen. Ähnlich 
wie in einem Adder-Tree und dann jede Ebene extra durch den Core 
quetschen. Dann hätte ich zwar eine hohe Taktrate, aber der effektive 
Durchsatz ist dann auch nicht so groß, da ich durch die Durchläufe im 
Addierer, das wieder zu nichte mache.

Es gibt einiges an Veröffentlichungen, die sich genau mit diesem Problem 
beschäftigen. Leider veröffentlichen sie nicht die Lösung als Code...

Jetzt weiß ich noch nicht, wie ich weitermache. Ich will noch nicht 
einsehen, dass der Core so vielleicht nicht einsetzbar ist, für meinen 
Zweck.

Grüße, Jens

von Rick D. (rickdangerus)


Lesenswert?

Ich würde ja erstmal eine langsame Lösung bauen, bevor ich gar keine 
Lösung habe...

von Martin S. (strubi)


Lesenswert?

Jens W. schrieb:
> da hast du Recht, das ist natürlich falsch. Und wird nicht
> funktionieren. Warum mein Simulationstool hier deutlich toleranter ist
> als deines, ist auch sehr interessant. Ich verwende noch ISE14.7 für den
> alten Kram und den eingebauten Simulator in ISE.

Oergs. isim ist "broken by design", d.h. es hat seit Jahren diverse 
VHDL-Standardverletzungen mitgeschleppt, damit's irgendwie mit der 
Legacy funktioniert. Das sorgt dann gern dafuer, dass manche Cores 
falsch rechnen (das alte signed/resize-Thema). Wenn du portablen 
sauberen Code entwickeln willst, tust du dir mit GHDL den besseren 
Gefallen. Sollte auch mit der XilinxCoreLib weitestgehend tun, kann 
sein, dass man ab ein illegales Statement auf den Standard trimmen muss. 
IMHO kommt man von dem CoreGen-Geraffel einfach am besten weg, ausser es 
nutzt strikt numeric_std.

Zum Pipelining:
Der Knoten steckt offensichtlich in deinem "Akkumulator", den du 
haendisch aus einem (latenz > 1) behafteten Addierer strickst. Das 
verletzt das 1-cycle-Akkumulatorprinzip grundsaetzlich :-)
Dir bleibt noch:

* (N-1) Addierer instanzieren (N = len(vector)). Das ist aber eher 
ungut: Zuviele Resourcen und allenfalls unnoetige Totzeiten.

* Wie schon oben vorgeschlagen die stumpfe float16-Pipeline durch 
Fixpoint-Arithmetik oder gar vereinfachte rationale Darstellung 
emulieren, also keine float16-Werte innerhalb der Pipeline nutzen. Spart 
auch massiv Resourcen und du hast mehr Kontrolle ueber allfaellige 
Fehlerabschaetzung. Damit schaffst du den 1-cycle-Akkumulator.

* Den Datenstrom per Interleaving so zu zerschnipseln und in L Bahnen 
und entsprechend L Zustaende (slots) deines Akkumulators aufzuteilen, 
dass du die volle Auslastung erreichst (L = Latenz deiner 
Verarbeitungs-Stufengruppe)

Das wird dann zu einem mehrdimensionalen Problem, wo es wirklich hilft, 
sich erst mal alles auf Karopapier aufzumalen, bevor du dir gleich eine 
ganze Latte an potentiellen Stolperfallen im Code schaffst.

Skalarprodukt ist 'nur' ein Spezialfall einer Matrixmultiplikation. Dazu 
gibt es haufenweise Ansaetze und Loesungen im Netz. Die Frage ist immer, 
wie genau deine Daten reintrudeln, und wie/wann du die Resultate 
brauchst.
Das sind deine Priority 1-Randbedingungen, die du skizzieren solltest, 
dann machst du dir typischerweise Gedanken zur 
Serialisierung/Interleaving/Multiplexing der Datenstroeme.

von Jens W. (jensw)


Angehängte Dateien:

Lesenswert?

Hallo Martin,

du scheinst mit ISIM schon deine Erfahrungen gemacht zu haben! ;-)
Das hat mich auch schon ordentlich geärgert, aber für meine Zwecke tut 
es das meist. Das Problem, das ich hier habe, ist auch eher ein 
Architektur-Problem und keines, dass die Cores nicht richtig 
funktionieren. In der Simulation tun sie genau das, was das Datenblatt 
verspricht.

Ein Ersatz der float16 in Fixpoint kommt nicht in Frage und macht auch 
in diesem Design keinen Sinn. Um eine ähnliche Auflösung zu bekommen 
bräuchte man irgendwas mit 30bit. Das ist bei der Multiplikation auch 
wieder schwierig, da die DSP48 nur 18x18bit können. Somit bräuchte ich 
für eine Multiplikation mehr als einen DSP48. Somit verringert sich 
direkt die Zahl der parallelen Berechnungen. Und wenn die Verschaltung 
der ganzen Geschichte von 16bit auf 30bit anwächst, kostet das auch 
Ressourcen, die nicht verfügbar sind im Chip.

Martin S. schrieb:
> Zum Pipelining:
> Der Knoten steckt offensichtlich in deinem "Akkumulator", den du
> haendisch aus einem (latenz > 1) behafteten Addierer strickst. Das
> verletzt das 1-cycle-Akkumulatorprinzip grundsaetzlich :-)
Das ist der Kern der Geschichte. Allerdings muss man nur das 
1-cycle-Prinzip auf ein LATENZ-cycle-Prinzip erweitern, dann 
funktioniert es wieder.

Ich habe nochmal von vorne angefangen. Bei Latenz=1 gilt das normale 
Akkumulator-Prinzip. Im nächsten Takt ist das Ergbenis aus In1+In2 am 
Ausgang vefügbar.
Wenn man sich nun eine 6-Stufige Pipeline vorstellt, dann kommt am 
Ausgang etwas raus wie, In1+In7, im nächsten Takt In2+In8 und so weiter. 
Das heißt man kann die Werte am Ende in einer Adder-Tree-Methode 
addieren und man hat das richtige Ergebnis. Und genau das ist der Punkt!
Es sind schon alle Werte im Akku drin (man hat ja auch alle 
reingefüttert), nur die kommen nicht in einem Ergebnis raus, sondern 
aufgeteilt in (in diesem Beispiel) sechs Werte.

In dem Bild im Anhang kann man es erkennen:
Hier ist die Pipeline 4 Stufen und am Eingang habe ich die Eingangswerte 
zur einfacheren Kontrolle geändert zu (1,2,3,4,5,6,7,8,9,10 --> Summe 
55). Die Gewichte sind immer 1, so dass die Multiplikation nicht 
reinspuckt.
Ich habe das mit mehreren Latenzen durchgespielt und das Ergebnis ist 
immer richtig, wenn ich, sobald alle Eingangswerte reingefüttert sind, 
die nächsten Werte, die der Latenz entsprechen, addiere.

Das Zusammenfassen, der Ausgangswerte ist jetzt noch nicht 
implementiert, das kommt als nächstes.

Grüße, Jens

von Martin S. (strubi)


Lesenswert?

Jens W. schrieb:
> Ein Ersatz der float16 in Fixpoint kommt nicht in Frage und macht auch
> in diesem Design keinen Sinn. Um eine ähnliche Auflösung zu bekommen
> bräuchte man irgendwas mit 30bit. Das ist bei der Multiplikation auch
> wieder schwierig, da die DSP48 nur 18x18bit können. Somit bräuchte ich
> für eine Multiplikation mehr als einen DSP48. Somit verringert sich
> direkt die Zahl der parallelen Berechnungen. Und wenn die Verschaltung
> der ganzen Geschichte von 16bit auf 30bit anwächst, kostet das auch
> Ressourcen, die nicht verfügbar sind im Chip.

Betr. "Auflösung" wird es eben lustig und richtig non-deterministisch 
mit float, du gewinnst zwar Dynamik, aber auf Kosten non-uniformer 
Auflösung. Und pikanterweise spielt es dann eine Rolle, in welcher 
Reihenfolge dein "interleaved Akkumulator" die Werte addiert. Kann man 
schon so machen, aber wo die Namensgebung Experimente mit Neuronalen 
Netzwerken suggeriert, würde ich sowas nur bedingt auf einer solchen 
Architektur aufbauen. Um dann auftretendes erratisches Verhalten bei 
Differenzierungen wieder zu kompensieren, brauchst du deutlich mehr 
Resourcen wegen zusätzlich nötiger Layer. Aber first things first.

Für deinen neuen Ansatz oben gilt also der Abschnitt "Interleaving". 
Innerhalb deiner gesamten Pipeline hast du ja beliebig die Möglichkeit, 
einen Datenstrom in mehrere Spuren aufzuteilen und wieder 
zusammenzuführen, indem du in den nachgeschalteten N-1 Pipeline-Stufen 
deine interleaved-Akkumulationen fürs Resultat addierst - unter den 
Randbedingungen deiner Vektorenlänge (die mir noch nicht ins Auge 
gesprungen ist). ABER: Wegen des Verlusts der Assoziativität 
unterscheidet sich das Resultat u.U. gröber von dem einer anderen 
Implementierung, z.B. Akkumulation (a0 + a1 + a2 + ...) statt (a0 + a6 + 
a1 + ...). Das sollte man in der Verifikation im Auge behalten.

von Rick D. (rickdangerus)


Angehängte Dateien:

Lesenswert?

Jens W. schrieb:
> Ein Ersatz der float16 in Fixpoint kommt nicht in Frage und macht auch
> in diesem Design keinen Sinn.
Das wage ich immer noch zu bezweifeln.

> Um eine ähnliche Auflösung zu bekommen
> bräuchte man irgendwas mit 30bit. Das ist bei der Multiplikation auch
> wieder schwierig, da die DSP48 nur 18x18bit können. Somit bräuchte ich
> für eine Multiplikation mehr als einen DSP48.
Ja, mit drei Stück DSP48 bekommt man einen 36x36 Bit Multiplizieren.

Hier hat sich mal jemand hingesetzt und geschaut, wie man für noch 
größere Bitbreiten die DSP48 effizient einsetzen kann:
https://ens-lyon.hal.science/ensl-00356421v1/document

Außerdem bekommst Du mit den DSP-Blöcken den Addierer gratis mit dazug:
https://docs.amd.com/r/2021.2-English/ug1483-model-composer-sys-gen-user-guide/DSP48E


Zum Thema Genauigkeit von float habe ich gerade die Tage erst ein 
schönes Beispiel bekommen:
1
#include <stdio.h>
2
#include <math.h>
3
4
int main( void)
5
{
6
    const int   n       = 1000;
7
    const float step    = 0.1;
8
    float       summe   = 0.0;
9
10
    for( int i = 0; i < n; i++)
11
    {
12
        summe += step;
13
    }
14
    printf( "%d * %f = %f\n", n, step, summe);
15
}

Bei mir sieht das so aus:
1
$ cc float_rundung.c && ./a.out
2
1000 * 0.100000 = 99.999046

von Martin S. (strubi)


Lesenswert?

Rick D. schrieb:
> Zum Thema Genauigkeit von float habe ich gerade die Tage erst ein
> schönes Beispiel bekommen:

Und noch schöner wird das Beispiel, wenn du `summe` auf einen sehr 
grossen Anfangswert setzt und dann damit das Endresultat vergleichst..
(Sei
..)

Was DSP-Blöcke angeht, kann man auch sparsame truncated multiplication 
implementieren. Je nach Architektur sind die daraus entstehenden 
Pipelines deutlich besser zu routen als mit den Haudrauf-Cores der 
klassischen Xilinx-FPGAs, die ihre DSP-Elemente eher auf periphere 
Verarbeitung von Daten ausgerichtet haben als auf kaskadierte Netze im 
"Fabric".

Aber das ist eher eine fortgeschrittene Geschichte, wo man ohne HLS 
nicht effektiv vorankommt.

Vorerst würde ich mich bei dem obigen Problem mal rein auf die 
Untersuchung der Pipeline fokussieren, dann auf die Arithmetik. Soll ja 
ein Lernprojekt sein, oder?

von Jens W. (jensw)


Lesenswert?

Hallo Rick,

danke für die Links!

Der Hintergrund, warum ich bei float bleibe, ist der, dass es sonst eben 
mehr DPS-Blöcke werden. Ich will ja parallel arbeiten und das so breit, 
wie es geht. Das FPGA, das ich nutze, hat 96 DSP-Blöcke. Die will ich 
nutzen um parallel zu arbeiten. Wenn ich pro Multiplikation 3 bräuchte, 
dann kann ich nur noch 30 Einheiten parallel laufen lassen.
Daher bleibe ich bei Float. Das bietet mir alles, was ich will. Die 
Auflösung passt, und da ist die Genauigkeit auch nicht so dramatisch, da 
es eh nur um Statistik geht und der Fehler ist einkalkuliert. Ich 
brauche mir keine großen Gedanken machen über Überläufe. Und die Cores 
laufen, wenn ich den Addierer fertig habe, mit 225Mhz (laut Tool, das 
ist natürlich noch nicht getestet). Da wird der Datendurchsatz sehr 
groß.
Wenn ich das mit nur 30 Einheiten parallel machen will, bräuchte ich die 
dreifache Frequenz und das wird auch nicht funktionieren. So schnell 
sind die Berechnungen in Fixpoint auch nicht.
Und, wenn man so viele Einheiten parallel verbinden will, macht es einen 
großen Unterschied, ob ich 16bit verbinden muss, oder 30bit.

Aber ich will keine Grundsatzdiskussion über Sinn und Unsinn von float 
anfangen. Hier habe ich mich entschieden, das so zu machen.
Ob ich damit auf die Nase falle, das werde ich dann auch sehen ;-)
Aber ohne blutige Nase lernt man ja nichts.

Bei deinem Beispiel kommt es auf die Implementierung an, also wie es im 
µC oder was auch immer umgesetzt ist. Bei meinen kleinen µCs erhalte ich 
immer 6 gültige Stellen. Danach wird gerundet. Allerdings würde ich mir 
schon erwarten, dass da dann 1000.00 als Ergebnis steht. Nicht 
99,irgendwas.
Da könnte auch was anderes faul sein...

Grüße, Jens

von Jens W. (jensw)


Lesenswert?

Martin S. schrieb:
> Vorerst würde ich mich bei dem obigen Problem mal rein auf die
> Untersuchung der Pipeline fokussieren, dann auf die Arithmetik. Soll ja
> ein Lernprojekt sein, oder?

Hallo Martin,

genau so ist es! Es geht rein ums Lernen. Ich mache das rein privat, 
daher habe ich auch kein Problem meinen Code zu posten.

Sobald ich das implementiert habe, melde ich mich mit dem Ergebnis.
Dann stelle ich mich auch gerne einem Benchmark ;-)
Wenn ihr Ideen habt, gerne her damit.

Danke euch!
Viele Grüße, Jens

von Martin S. (strubi)


Lesenswert?

Jens W. schrieb:
> Bei deinem Beispiel kommt es auf die Implementierung an, also wie es im
> µC oder was auch immer umgesetzt ist. Bei meinen kleinen µCs erhalte ich
> immer 6 gültige Stellen. Danach wird gerundet. Allerdings würde ich mir
> schon erwarten, dass da dann 1000.00 als Ergebnis steht. Nicht
> 99,irgendwas.
> Da könnte auch was anderes faul sein...

Nein, das ist ein inhärentes Problem der float-Arithmetik. Wenn die nach 
IE³-754 implementiert ist, ist das auf jedem uC/CPU gleich "falsch".

Deswegen spielt es bei float eine grosse Rolle, in welcher Reihenfolge 
du die Produkte addierst. Wird dir dann nämlich passieren, dass:

wenn (a viel kleiner als A).

Ich würde mal schwer wetten, dass du mit der Duplizierung deiner 
Pipeline die f_max rapide nach unten drücken musst und die 
DSP48-Resourcen so gar nicht optimal nutzen kannst. xst macht dann u.U. 
ab einem Punkt auch nur noch nerviges Zeug, was man nicht will. Aber 
kannst ja dann mal Bilder vom PnR posten, wenn die Flaschenhälse 
auftreten. Du nutzt ja hoffentlich nicht einen alten Spartan3E, oder?

von Jens W. (jensw)


Lesenswert?

Martin S. schrieb:
> Du nutzt ja hoffentlich nicht einen alten Spartan3E, oder?

Nee, das nicht. Aber so viel neuer ist es auch nicht. Ich verwende ein 
Virtex-4. ;-)
Ich hab das mal günstig auf ebay gefunden und für meine Gehversuche tut 
es das hoffentlich.
https://www.ebay.com/itm/186917057793


Grüße

Beitrag #8039481 wurde vom Autor gelöscht.
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.