This commit is contained in:
Thomas Keller 2023-11-10 16:05:21 +01:00
parent 3b4b313dc4
commit b852ff9ea3
21 changed files with 1069 additions and 2 deletions

175
Cluster-Getting-Started.md Normal file
View File

@ -0,0 +1,175 @@
# Lithium - ein HPC Cluster für die Fachhochschule Graubünden
## Hardware
Für Berechnungen in der Forschung und Lehre kann die Fachhochschule Graubünden seit Herbst 2023 auf einen High Performance Computing (HPC) Cluster zurückgreifen. Dieser wurde aufgebaut um die Lehre und Forschung bei grossen Datenmengen und rechenintensiven Aufgaben zu Untersützen und die Studierenden mit den Prinzipien des verteilten Rechnens bekannt zu machen.
Der HPC Cluster entspricht in seinem Aufbau einem [Beowulf Cluster][1]. Dieser nutzt spezialisierte Softwarebibliotheken aus dem [OpenHPC][2] Repository für das verteilte Rechnen. Zur Verwaltung des Clusters verwenden wir die Software [Warewulf][1] in einer sogenannten 'stateless' Konfiguration. Dadurch ist sichergestellt, dass die Knoten (einzelne Cluster Computer) softwaremässig identisch installiert und konfiguriert sind, was verhindert, dass es zu Versionsdifferenzen auf den einzelen Knoten kommt.
Der Cluster basiert auf einer von der Firma Sanitas gespendeten und vom DAViS/CDS auf Open Source Komponenten umgenutzten [Oracle Big Data Applicance X5-2L][4] mit sechs Nodes (Knoten).
Ein Node besteht aus den folgenden Komponenten
<table>
<tr>
<th>2 x CPU</th>
<td>Xeon E5-2699 v3 @ 2.30GHz</td>
</tr>
<tr>
<th>RAM</th>
<td>128 GB DDR-4</td>
</tr>
<tr>
<th>Disks</th>
<td>12 x 14 TB 7200 RPM SAS</td>
</tr>
</table>
und sieht so aus:
![Beispiel eines Nodes](./images/cluster/Clusternode-Front.png)
Der gesamte Cluster mit Netzwerkequipment so:
![FH Cluster](./images/cluster/Cluster.png)
Für die Intranode-Kommunikation beim Rechnen über RDMA (zum Beispiel MPI) kommt ein High Performance Interconnect auf Basis von Infiniband QDR (40 Gbps) zum Einsatz. Für die Verwaltung der Nodes (SSH, TFTP etc.) steht ein dediziertes 10 GB Ethernet Netz zur Verfügung.
Im Hardwarevergleich steht unser Rechner hier:
| | Notebook | Lithium | CSCS Alps
|----------------------|-----------|-----------|-----------
| **Physische Cores** | 4 Cores | 216 Cores | 65536 Cores
| **Memory (RAM)** | 16 GB | 768 GB | ?
| **Speicherplatz** | 1 TB | 504 TB | 10 PB
| **Energieverbrauch** | 60 W | 5-6 KW | 5-10 MW
| **Interconnect** | 1 Gb/s | 40 Gb/s | 200 Gb/s
Das bedeutet, dass wir hardwaremässig sehr viel kleiner sind als Cluster im Bereich der [Top500][10]. Auf der Anwenderseite sind wir jedoch sehr ähnlich ausgerüstet wie die grossen HPC Forschungscluster. Auf Lithium findet man daher unter anderem folgende Komponenten:
* Slurm für das Job Scheduling
* BeeGFS (paralleles Filesystem) auf dem Scratchlaufwerk /scratch
* MPICH für das verteilte Rechnen
* Warewulf für die Clusteradministration
Im gesamten sind über 300 HPC spezifische Softwarepakete installiert.
## Architektur
Der Fachhochschulcluster besteht aus sechs Knoten (engl. Nodes). Dabei sind die einzelnen Nodes über ein Low Latency, High Bandwith [RDMA Netzwerk][5] miteinander verbunden. Dieses Netzwerk kommt hauptsächlich während einer Berechnung zum Einsatz.
Einer der sechs Nodes ist als "Master", respektive "Headnode" oder "Loginnode" konfiguriert. Dieser ist während einer Berechnung für Koordinations- und Steueraufgaben zuständig und wird zudem zur Verwaltung des Clusters verwendet. Auf diesem Node wird prinzipiell *nicht* gerechnet.
Die eigentliche Rechenaufgabe wird für die parallele Abarbeitung auf die Computenodes verteilt. Nach Abschluss der Berechnung werden die Resultate entweder automatisch durch das verwendete Skript oder manuell über das /scratch oder Homelaufwerk zurück an den Masternode geschickt.
Der Fachhochschule Cluster ist wie folgt aufgebaut:
<img src="./images/cluster/Clusterarchitektur.png" alt="Cluster Architektur" width="600"/>
## Zugriff auf den Cluster
Auf den Cluster kann nur mit SSH zugegriffen werden. Momentan können sich alle Mitarbeitenden des SIIs und alle Studierenden des Studiengangs CDS darauf einloggen. Für den Login braucht es einen SSH Client und einen FHGR Account. Der Zugriff mit SSH funktioniert so:
```
ssh <FH Benutzername>@lithium.fhgr.ch
```
Falls Du den Cluster verwenden möchtest, jedoch nicht zum SII oder CDS Studiengang gehörst, melde dich bitte beim DAViS Admin (thomas.keller@fhgr.ch).
## Starten der Computenodes
Um Energie zu sparen, werden unbenutzte Computenodes automatisch heruntergefahren. Um eine Berechnung zu starten, ist es daher notwendig, dass die Computenodes gebootet sind und sich im Slurmstate 'IDLE' befinden. Sollte der Status mit dem Befehl ```sinfo --all``` als 'down' angezeigt werden, dann müssen die Computenodes gestartet werden. Das Starten aller Nodes ist mit dem folgenden Befehl möglich:
```
sudo /usr/local/bin/start-computeNodes.bash
```
Nach einigen Minuten, sollte der Status des Clusters von 'DOWN' auf 'IDLE' wechseln. Sollte einer der folgenden Status anzeigt werden 'IDLE\*' (mit Stern), 'DRAINED', 'DRAINING', 'FAIL', 'FAILING', 'FUTURE', 'POWER_DOWN', 'UNKNOWN', 'RESERVED' dann informiere bitte, den DAViS Administrator.
## Job Scheduler und Partitionen
Da ein Cluster auch ein Mehrbenutzersystem ist, können dessen Resourcen nicht jederzeit frei verwendete werden. Daher kommt ein Jobscheduler zum Einsatz, der Rechenjobs mit den zur Verfügung stehenden Ressourcen möchglichst optimal zur Ausführung bringt. Auf lithium.fhgr.ch werden die Jobs durch [Slurm][6] verwaltet.
Grundsätzlich folgt Slurm auf Lithium dem [FIFO mit Backfill][7] Prinzip. Vereinfacht gesagt bedeutet das, dass der erste eingereichte Job zuerst abgearbeitet wird. Weiter wird die Ressourcennutzung durch sogenannte Partitionen eingeschränkt. Diese bestimmen wie lange ein Benutzer einen Slurm-Job ausführen darf. Momentan sind die folgenden Partitionen konfiguriert:
| Anwendergruppe / Partitionen | Debug | Studierende | Mitarbeitende
|------------------------------|-------|--------------|---------------
| Studierende | 5 Min | 3 h | kein Zugriff
| Mitarbeitende | 5 Min | kein Zugriff | 12 h
Eine Berechnung die länger als die durch die Partition vorgegebene Zeit läuft wird **abgebrochen**. Diese Limite ist dazu da, damit ein Benutzer nicht irrtümlich oder absichtlich den Cluster für eine unbegrenzte Zeit blockieren kann. Daher empfiehlt es sich dringend, im Skript sogenannte 'Checkpoints' zu implementieren. Wie Checkpoints im Falle von Tensorflow oder Keras implementiert werden, findest Du [hier][11]. Checkpoints schützen übrigens auch vor einem Zitverlust bei einem Stromausfall.
Falls Du deutlich mehr als die oben erwähnten Zeitspannen für eine Berechnung brauchst, melde dich beim DAViS Admin.
## Clusterfilesystem und Homeverzeichnis
Auf dem Cluster wird das Homeverzeichnis von jedem Benutzer automatisch mit [NFS][9] auf die Computenodes exportiert. Das heisst, dass auf jedem Node, egal ob Master oder Computenode die Daten des Homeverzeichnisses zur Verfügung stehen. Übrigens: auf deinem Homeverzeichnis steht jedem Anwender 80 GB Speicherplatz zur Verfügung. Wieviel Speicherplatz Du bereits belegt hast, kannst Du mit dem Befehl `quota -fs /` ausfindig machen.
Des weiteren gibt es unter dem Verzeichnis `/scratch` ein geshartes Filesystem mit einer Grösse von ca. 500 TB. Hier kommt BeeGFS, ein paralleles Filesysteme zum Einsatz das Speicherplatz von allen Computenodes unter dem Mountpoint (Verzeichnis) `/scratch` vereint. Daher kann es nur genutzt werden wenn alle Computenodes gestartet sind. Wie das Homverzeichnis ist auch das /scratch Laufwerk von allen Nodes zugreifbar.
Bitte beachte, dass Dateien unter dem Verzeichnis `/scratch` von allen Clusterbenutzern gelesen aber nur vom Dateieigentümer verändert werden können.
## Stromverbrauch und Energiesparmodus
Mit dem Befehl
```
sudo /usr/local/bin/get-powerUsage.bash
```
kann der momentane Stromverbrauch des Clusters in Erfahrung gebracht werden. Dabei ist der Stromverbrauch des High Performance Transports (in unserem Falle Infiniband) nicht mit eingerechnet.
Der Energiesparmodus wird aktiviert, falls der letzte Slurm Job vor mehr als 90 Minuten abgeschlossen wurde oder die Clusternodes erst kürzlich gebootet wurden. Solange Slurmjobs ausgeführt werden, werden die belegeten Clusternodes nicht in einen Energiestparmodus versetzt oder ausgeschaltet.
## Installation von zusätzlicher Software
Viele Machinlearning und Deeplearning Frameworks oder Python Module können über Anaconda, Miniconda oder Apptainer installiert werden. Für Anhaltspunkte wie das gemacht werden kann, siehe die Anleitung [Softwareinstalltionen auf den Workstations][8]
## Erste Schritte auf dem Cluster
Mit dem Befehl srun kann ein Slurmjob auf dem Cluster ausgeführt werden. Als simples Beispiel dient hier der Befehl 'hostname', der den Namen des lokalen Rechners anzeigt.
```
srun -p students -n $((36*5)) -N5 hostname
```
Mit der Option `-p` wird die Partition ausgewählt, im obigen Fall die 'students' Partition. Mit der Option `-n` teilen wir Slurm mit, wieviele parallele Tasks (Prozesse) wir ausführen wollen. Da wir pro Computenode 36 physische Cores haben (Hyperthreading ist deaktiviert) können wir auf dem Cluster maximal 180 Prozesse starten. Die Option `-N` teilt Slurm mit wieviele Computenodes für die Berechnung verwendet werden sollen. Im obigen Fall sollten wir daher genau 180 Zeilen Output erhalten.
Mit dem Befehl ```squeue``` kann angezeigt werden welche Jobs momentan ausgeführt werden und welche Jobs auf Ressourcen warten.
Für nicht-interaktive und länger laufende Jobs ist es sinnvoll `sbatch` zu verwenden. Damit muss nicht gewartet werden bis der Cluster frei wird, sondern der Jobscheduler informiert den Anwender per Mail sobald das Skript zur Ausführung kommt.
Dafür muss ein Shellskript geschrieben werden das einerseits einen Abschnitt enthält mit Informationen für Slurm (diese Befehle sind mit `#SBATCH` gekenntzeichnet) andererseits einen Abschnitt der den Befehl zum Starten der Berechnungen enthält. Dies könnte zum Beispiel so aussehen:
```
#!/bin/bash
#SBATCH --nodes=5 ## Number of computenodes
#SBATCH --output="slurm-%j.out" ## Logfile
#SBATCH --time=240:00 ## Time limit. Should be equal or smaller than the partitions time limit
#SBATCH --job-name="test-mpi" ## Job name. This will be used in naming directories for the job.
#SBATCH --partition=staff ## Partition to launch job in
#SBATCH --cpus-per-task=1 ## The number of threads the code will use
#SBATCH --ntasks-per-node=1 ## Number of tasks (processes/threads) to run
#SBATCH --mail-type=BEGIN,END ## Sends a Email when Job starts and when the Job ends
#SBATCH --mail-user=<mail address> ## Mail Address
# Load an environment
module load py3-mpi4py
# Execute the python script and pass the argument '90'
srun python3 my-mpiProg.py 90
```
[1]: https://de.wikipedia.org/wiki/Beowulf_(Cluster) "Beowulf Cluster"
[2]: https://openhpc.community/ "OpenHPC"
[3]: https://warewulf.org/ "Warewulf"
[4]: https://docs.oracle.com/en/servers/x86/x86-server-x5-2l/ "Handbücher"
[5]: https://en.wikipedia.org/wiki/Remote_direct_memory_access
[6]: https://slurm.schedmd.com/
[7]: https://slurm.schedmd.com/sched_config.html
[8]: https://gitea.fhgr.ch/kellerthomas/docs-cds/src/branch/master/Installation-Tensorflow.md
[9]: https://de.wikipedia.org/wiki/Network_File_System
[10]:https://top500.org/
[11]:https://www.tensorflow.org/guide/checkpoint

248
Cluster-PySpark-Howto.md Normal file
View File

@ -0,0 +1,248 @@
# PySpark auf dem HPC Cluster
In diesem Beispiel verwenden wir Magpie um einen Sparkcluster innerhalb des Lithium HPC Clusters aufzubauen.
## Zielpublikum
Dieses Dokument ist für fortgeschrittene Anwender gedacht. Du solltest daher mit den Grundsätzen unseres HPC Clusters vertraut sein (siehe [Cluster Howto][1]) und Übung im Umgang mit der Linux Konsole und SSH haben.
Diese Anleitung ist eine ausgedeutsche und auf unsere Umgebung zugeschnittene Version der offiziellen [Magpie Dokumentation][2]
## Grundsätzliches Vorgehen
- Installation der Softwarekomponenten für Spark
- Konfiguration dieser Softwarekomponenten
- Mit Slurm ein Magpie Batchskript aufgeben
- Zugriff auf den Cluster mit SSH
## Installation und Konfiguration von Spark
Klonen der neusten Magpie Skripte ab Github:
```
mkdir spark && cd spark
git clone https://github.com/LLNL/magpie.git
cd magpie/misc/
vim magpie-download-and-setup.sh
```
Passe im File `magpie-download-and-setup.sh` die folgenden Parameter an und lösche "#" bei Bedarf vor diesen Einträgen:
```
SPARK_DOWNLOAD="Y"
INSTALL_PATH="$HOME/spark"
PRESET_LAUNCH_SCRIPT_CONFIGS="Y"
LOCAL_DIR_PATH="/tmp/$USER"
NETWORKFS_DIR_PATH="/scratch/$USER"
```
Danach das Skript mit dem Befehl `./magpie-download-and-setup.sh` ausführen. Während des eher gemächlichen Downloads von Spark kann die Zeit genutzt werden, um herauszufinden welche Version von Python und Java die verwendete Spark Version (in unserem Fall 3.3.2) voraussetzt.
Diese Informationen können wir am Zuverlässigsten von der offiziellen Spark Webseite entnehmen. Im Falle von Spark 3.3.2 ist diese Information unter https://spark.apache.org/docs/3.3.2/ zu finden. Daher können wir folgende Prerequisits notieren:
* Java 17
* Python 3.7 und neuer
Diese Softwarepakete installieren wir mit Miniconda - Lizenzvereinbarungen akzeptieren und Installationsorte übernehmen:
```
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
bash Miniconda3-latest-Linux-x86_64.sh
rm Miniconda3-latest-Linux-x86_64.sh
```
Nach Abschluss der Installation die Datei `.bashrc` neu laden
```
. ~/.bashrc
```
Jetzt können wir eine Conda Environment anlegen und die entsprechenden Versionen von Java und Python installieren. Ìn unserem Beispiel mit **Spark 3.3.2** sieht das so aus:
```
conda create -y -n spark
conda activate spark
conda install -y python=3.11.4
conda install -y -c conda-forge openjdk=17.0.8
```
Danach mit den Befehlen (innerhalb der conda env)
```
type -p python
dirname $(dirname $(readlink -f $(type -p java)))
```
die **jeweiligen Speicherorte des Python und Java executable anzeigen lassen und notieren**. Die Pfade sollten die Verzeichniselement miniconda3/envs enthalten. Nachdem das Skript `./magpie-download-and-setup.sh` seine Arbeit abgeschlossen hat, wechseln wir mit folgenden Befehlen zurück ins Verzeichnis `~/spark` und führen "magpie.sbatch-srun-spark" aus:
```
cd $HOME/spark
cp magpie/submission-scripts/script-sbatch-srun/magpie.sbatch-srun-spark .
```
Im Submission-Skript `magpie.sbatch-srun-spark` passen wir nun die folgenden Zeilen an; je nach benötigten Resourcen, können die Parameter zu den SBATCH Befehlen individuell angepasst werden.
**Achtung: die Rautenzeichen müssen bestehen bleiben.**
```
#SBATCH --nodes=5
#SBATCH --time=180:00
#SBATCH --job-name="spark-test"
#SBATCH --partition=staff ODER #SBATCH --partition=students
export MAGPIE_JOB_TYPE="interactive"
export JAVA_HOME="<gemäss des Outputs mit type -a java, siehe oben>"
export MAGPIE_PYTHON="<gemäss des Outputs mit type -a python, siehe oben>"
export SPARK_LOCAL_SCRATCH_DIR="/tmp/${USER}/sparkscratch/"
```
Als nächstes können wir nun das Magpie Skript als Slurmjob an den HPC Cluster übergeben. Zuerst überprüfen wir nochmals, ob das Cluster Lithium online ist und Ressourcen frei sind:
```
sinfo --all
```
Falls genügend Nodes (in unserem Beispiel fünf) im Status 'Idle' erscheinen, können wir sofern wir uns noch im Verzeichnis "/home/-user-@edu.local/spark" befinden, den Slurmjob mit dem Befehl
```
sbatch -k magpie.sbatch-srun-spark
```
dem Slurm Scheduler übergeben. Mit dem Befehl `squeue` können wir den Status unseres Batchjobs in Erfahrung bringen. Idealerweise ist dieser auf 'R' (running) oder 'PD' (pending).
Ob unser Sparkcluster bereits gestartet ist, erfahren wir im Slurmjob Logfile im Verzeichnis `spark`. Zum Beispiel mit folgendem Befehl mit der Nummer des Bashjobs für *:
```
cat $HOME/spark/slurm-*.out
```
Im Erfolgsfalle sollten unter anderem die untenstehenden Angaben im Logfile angezeigt werden:
```
.
.
.
* ssh computenode1
* export JAVA_HOME="$HOME/miniconda3/envs/spark/bin"
* export SPARK_HOME="$HOME/spark/spark-3.4.1-bin-hadoop3"
* export SPARK_CONF_DIR="/tmp/$USER/spark/spark-test/*/spark/conf"
*
* Then you can do as you please. For example to run a job:
*
* $SPARK_HOME/bin/spark-submit --class <class> <jar>
*
*******************************************************
*******************************************************
* Executing Pre Run Scripts
*******************************************************
*******************************************************
* Entering Magpie interactive mode
*******************************************************
*******************************************************
* Run
*
* ssh computenode1 kill -s 10 6636
*
* to exit 'interactive' mode early.
*******************************************************
```
In diesem Logfile interessiert uns hauptsächlich wie wir nun auf die PySpark Shell zugreifen können. Wie in der `slurm-*. out` Datei erwähnt, können wir uns nun mit den folgenden Befehlen aus dem Logfile einloggen:
```
ssh computenode1
export JAVA_HOME="$HOME/miniconda3/envs/spark/bin"
export SPARK_HOME="$HOME/spark/spark-3.4.1-bin-hadoop3"
export SPARK_CONF_DIR="/tmp/$USER/spark/spark-test/*/spark/conf"
conda activate spark
```
Danach kann ein beliebiger PySpark Befehl ausgeführt werden. Zum Beispiel eine Sparkshell:
```
$SPARK_HOME/bin/pyspark
```
## Installation von SparkNLP auf PySpark
Wie auch andere Python-Bibliotheken kann beispielsweise auch die auf Spark-basierende NLP-Library [SparkNLP][3] über die aufgesetzte virtuelle Umgebung "spark" via pip oder conda installiert werden. Für SparkNLP werden als Ergänzung der Miniconda-Basisumgebung noch folgende Bibliotheken benötigt:
* NumPy
Nach der Installation von Numpy kann nun auch SparkNLP in der virtuellen Umgebung "spark" mit folgendem Befehl installiert werden:
```
pip install spark-nlp
```
### SparkNLP mit einem vortrainierten Modell
Starten des Batchjobs:
```
sbatch -k magpie.sbatch-srun-spark
```
Danach folgt der übliche Ablauf um Pyspark zu starten, jedoch müssen wir gleichzeitig die [jar-packages][3] von SparkNLP beim Aufstarten von Pyspark laden:
**Achtung: Die Version des Package muss mit der installierten SparkNLP Version übereinstimmen. (hier SparkNLP 5.1.1)**
```
ssh computenode1
export JAVA_HOME="$HOME/miniconda3/envs/spark/bin"
export SPARK_HOME="$HOME/spark/spark-3.4.1-bin-hadoop3"
export SPARK_CONF_DIR="/tmp/$USER/spark/spark-test/*/spark/conf"
conda activate spark
$SPARK_HOME/bin/pyspark --packages com.johnsnowlabs.nlp:spark-nlp_2.12:5.1.1
```
#### Beispiel
Nach erfolgreichem Start von SparkNLP und der Sparkshell können wir ein vortrainiertes Modell zu einem Named Entity Recognition-Task mit folgendem [Beispielcode][4] verwenden:
```
import json
import numpy as np
import sparknlp
import pyspark.sql.functions as F
from pyspark.ml import Pipeline
from pyspark.sql import SparkSession
from sparknlp.annotator import *
from sparknlp.base import *
from sparknlp.pretrained import PretrainedPipeline
from pyspark.sql.types import StringType, IntegerType
# If you change the model, re-run all the cells below
# Other applicable models: ner_dl, ner_dl_bert
MODEL_NAME = "ner_dl_bert"
text_list = ["""William Henry Gates III (born October 28, 1955) is an American business magnate, software developer, investor, and philanthropist. He is best known as the co-founder of Microsoft Corporation. During his career at Microsoft, Gates held the positions of chairman, chief executive officer (CEO), president and chief software architect, while also being the largest individual shareholder until May 2014. He is one of the best-known entrepreneurs and pioneers of the microcomputer revolution of the 1970s and 1980s. Born and raised in Seattle, Washington, Gates co-founded Microsoft with childhood friend Paul Allen in 1975, in Albuquerque, New Mexico; it went on to become the world's largest personal computer software company. Gates led the company as chairman and CEO until stepping down as CEO in January 2000, but he remained chairman and became chief software architect. During the late 1990s, Gates had been criticized for his business tactics, which have been considered anti-competitive. This opinion has been upheld by numerous court rulings. In June 2006, Gates announced that he would be transitioning to a part-time role at Microsoft and full-time work at the Bill & Melinda Gates Foundation, the private charitable foundation that he and his wife, Melinda Gates, established in 2000.[9] He gradually transferred his duties to Ray Ozzie and Craig Mundie. He stepped down as chairman of Microsoft in February 2014 and assumed a new post as technology adviser to support the newly appointed CEO Satya Nadella.""", """The Mona Lisa is a 16th century oil painting created by Leonardo. It's held at the Louvre in Paris."""]
documentAssembler = DocumentAssembler().setInputCol('text').setOutputCol('document')
tokenizer = Tokenizer().setInputCols(['document']).setOutputCol('token')
# ner_dl and onto_100 model are trained with glove_100d, so the embeddings in
# the pipeline should match
if (MODEL_NAME == "ner_dl") or (MODEL_NAME == "onto_100"):
embeddings = WordEmbeddingsModel.pretrained('glove_100d').setInputCols(["document", 'token']).setOutputCol("embeddings")
# Bert model uses Bert embeddings
if MODEL_NAME == "ner_dl_bert":
embeddings = BertEmbeddings.pretrained(name='bert_base_cased', lang='en').setInputCols(['document', 'token']).setOutputCol('embeddings')
ner_model = NerDLModel.pretrained(MODEL_NAME, 'en').setInputCols(['document', 'token', 'embeddings']).setOutputCol('ner')
ner_converter = NerConverter().setInputCols(['document', 'token', 'ner']).setOutputCol('ner_chunk')
nlp_pipeline = Pipeline(stages=[documentAssembler, tokenizer, embeddings, ner_model, ner_converter])
df = spark.createDataFrame(text_list, StringType()).toDF("text")
result = nlp_pipeline.fit(df).transform(df)
```
Die Resultate des verwendeten Modells sind nun geordnet in einem verschachtelten RDD-Dataframe gespeichert. Die Inhalte dieses Dataframe können wir mit unterschiedlichsten Spark Methoden ausgeben:
```
#Gibt den vollständigen Inhalt als String aus
result.collect()
#Zeigt eine Übersicht zur RDD-Struktur
result.show()
#Zeigt die Token bzw. Wörter zusammen mit den vorausgesagten NER-Labels in einer Tabelle
result.select('token.result','ner.result').show(truncate=100)
```
tbc
[1]: https://gitea.fhgr.ch/kellerthomas/docs-cds/src/branch/master/Cluster-Howto.md
[2]: https://github.com/LLNL/magpie/blob/master/doc/README.spark
[3]: https://sparknlp.org/api/python/getting_started/index.html
[4]: https://colab.research.google.com/github/JohnSnowLabs/spark-nlp-workshop/blob/master/tutorials/streamlit_notebooks/NER_EN.ipynb

204
Installation-Tensorflow.md Normal file
View File

@ -0,0 +1,204 @@
# Softwareinstalltionen auf den Workstations
Da es sich bei allen Rechnern um **Mehrbenutzersysteme** handelt, ist der Zugriff mit **su oder sudo auf den Workstations abgeschaltet**. Um zusätzliche Software für Berechnungen wie Tensorflow zu installieren, gibt es folgende zwei Möglichkeiten:
- Apptainer
- Miniconda
*Hinweis: Docker ist auf den HPC Workstations nicht verfügbar. Docker-Container können jedoch mit der Software 'Apptainer' ausgeführt werden*
## Apptainer
Apptainer (ehemals Singularity) ist ein *Containersystem* das für den Einsatz auf HPC Systemen optimiert ist. Apptainer unterstützt verschiedene Clustertechnologien wie Infiniband, SLURM oder MPI. Da Apptainer den OCI Standard unterstützt, können neben Apptainerimages auch Images von Dockerhub oder Nvidia (https://catalog.ngc.nvidia.com/containers) unter Apptainer ausgeführt werden.
Auf unseren Workstations kann damit in einem Container eine Runtime für GPU und CPU Berechnungen mit Tensorflow, Pytorch, etc. ausgeführt werden.
Neben diesen vorbereiteten Images ist es auch möglich, einen Container von Grund auf neu zu bauen, falls es kein entsprechendes Image mit der benötigten Software gibt. Da dies jedoch ein aufwendigeres Unterfangen ist, wird an dieser Stelle nur auf die Dokumentation verwiesen. Diese ist unter https://apptainer.org/docs/user/main/quick_start.html#building-images-from-scratch zu finden.
## Miniconda
Miniconda ist eine virtuelle Environment (offiziell eine Distribution) über die im Homeverzeichnis mit dem Paketmanager `conda` Software für die Programmierung im Data Science Bereich installiert werden kann. Wie Miniconda und Pakete für Tensorflow installiert werden können, ist im Abschnitt *Installation von Software mit Miniconda* und *Installieren von Tensorflow mit Conda* beschrieben
### Installieren von Tensorflow mit Apptainer
Damit Tensorflow die GPUs auf den Workstations zur Berechnung nutzt, **müssen verschiedene Komponenten** im Container und auf der Workstation vorhanden sein. Grundsätzlich sind dies:
| ![Container GPU Stack](./images/docker-nvidia-gpu-arch.png)
|:--:
| Software Stack für Docker/Apptainer GPU Berechnungen
Auf den Workstations bereits installiert sind:
* GPU Treiber (Nvidia Treiber)
* CUDA
Wie aus der Grafik ersichtlich ist, brauchen wir im Container Tensorflow. Dieses könnten wir von Hand in den Container installieren, z.B. in dem Fall, wenn eine spezifizierte Version benötigt wird. Es gibt jedoch eine grosse Pallette an vorgefertigten Images von Dockerhub oder Nvidia. Um uns das Ingeunieursleben etwas zu erleichtern, verwenden wir folgend einen vorgefertigten Container.
Falls uns jedoch im Container noch ein bestimmtes Softwarepaket fehlt, ist es möglich, mit Hilfe einer Container-Sandbox dieses zu installieren und danach ein modifiziertes und auf unsere Bedürfnisse angepasstes Containerimage zu generieren.
Auf einer der oben erwähnten Workstations mit SSH einloggen und mit dem folgenden Befehl eine Verzeichnishierarchie für den Buildvorgang erstellen:
```
mkdir -p ~/build-apptainer/sandboxes/ && cd ~/build-apptainer
```
Container mit bereits installierter Tensorflowsoftware können von Dockerhub oder von der Nvidia-Registry bezogen werden. Dabei wird das Docker Imageformat automatisch ins Apptainerformat (.sif) umgewandelt. Im nachfolgenden Beispiel benutzen wir die Docker-Registry:
```
apptainer pull tensorflow-2.12.0-gpu.sif docker://tensorflow/tensorflow:2.12.0-gpu
```
Der vorherige Befehl speichert das Tensorflowimage im Homeverzeichnis unter build-apptainer ab. Falls Apptainer sich beim Download des Images über mangelnden Speicherplatz beklagt, gibt es unter dem Abschnitt "Troubleshooting" Tipps, wie in einem solchen Fall vorgegangen werden kann.
Als nächstes bauen wir ein neues Tensorflowimage und installieren als Beispiel das Paket nvidia-profiler in das Image nach. Dazu benutzen wir als Basis das bereits heruntergeladene Tensorflowimage (tensorflow-dockerhub.sif) das wir bereits mit `aptainer pull` in unser Homeverzeichnis gespeichert haben:
```
apptainer build --sandbox sandboxes/tensorflow ./tensorflow-2.12.0-gpu.sif
apptainer exec --fakeroot --writable sandboxes/tensorflow/ apt-get update
apptainer exec --writable --fakeroot sandboxes/tensorflow/ apt install -y nvidia-profiler
```
Da auf den Homedirectories eine Diskquota von 80GB aktiviert ist, kann es sein, dass wir dadurch zu wenig Speicherplatz haben um das Image zu bauen. Daher speichern wir das neu erstellte Tensorflowimage unter /scratch:
```
mkdir -p "/scratch/${USER}/"
apptainer build "/scratch/${USER}/tensorflow-2.12.0-gpu-modified.sif" sandboxes/tensorflow/
```
Danach können wir interaktiv im Container arbeiten. Apptainer mountet das Homeverzeichnis automatisch in den Container, daher stehen unsere Skripts die wir für eine Berechnung brauchen automatisch zur Verfügung.
Mit dem Befehl ```apptainer shell``` können wir ein Apptainerimage öffnen und darin arbeiten.
```
apptainer shell --nv "/scratch/${USER}/tensorflow-2.12.0-gpu-modified.sif"
```
Jetzt könne wir überprüfen ob die GPUs im Container korrekt erkannt werden:
```
python3 <<- EOF
import tensorflow as tf
print(tf.config.list_physical_devices('GPU')[0])
EOF
```
Der Output sollte dann, je nach Anzahl GPUs, wie folgt aussehen:
> PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')
## Installation von Software mit Miniconda
```
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
bash Miniconda3-latest-Linux-x86_64.sh
rm Miniconda3-latest-Linux-x86_64.sh
```
Danach die .bashrc nochmals neu laden (dies ist nur einmalig nötig)
```
. ~/.bashrc
```
Neue virtuelle Umgebung erstellen und aktivieren
```
conda create --name <ENVNAME>
conda activate <ENVNAME>
```
Die verfügbaren virtuellen Umgebungen können mit `conda env list` angezeigt werden.
Jetzt kann Software, zum Beispiel Pandas oder Tensorflow, wie folgt installiert werden:
```
conda search <PKGNAME>
conda install <PKGNAME>
```
Mit conda installierte Pakete können bei Bedarf mit dem 'update' Befehl aktualisiert werden. Für Python 3 wäre das:
```conda update python3```
Update aller Pakete in der momentan aktivierten Conda Environment:
```conda update --all```
Update von Conda selbst:
```conda update -n base -c defaults conda```
## Installieren von Tensorflow mit Conda
Als erstes erstellen wir eine Conda Environment die Python 3.9 enthält:
```
conda create --name tf -y python=3.9
conda activate tf
```
Damit Tensorflow die GPUs nutzen kann, müssen die Treiber korrekt installiert sein. Dies können wir mit dem Befehl `nvidia-smi` überprüfen:
| ![Nvidia SMI Output](./images/console-nvidia-smi.png)
|:--:
| Output im Falle von korrekt erkannter GPU
Danach installieren wir das Cuda Toolkit und CuDNN:
```
conda install -c conda-forge -y cudatoolkit=11.8.0
pip install nvidia-cudnn-cu11==8.6.0.163
CUDNN_PATH=$(dirname $(python -c "import nvidia.cudnn;print(nvidia.cudnn.__file__)"))
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$CONDA_PREFIX/lib/:$CUDNN_PATH/lib
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH#:}
```
Mit den letzten drei Befehlen werden die Variabeln **LD_LIBRARY_PATH** und **CUDNN_PATH** gesetzt. Tensorflow funktioniert nur richtig, wenn diese Variabeln korrekte Pfade enthalen. Deren Output sieht in etwa so aus (kann je nach OS, eingeloggtem Benutzer und Version etwas abweichen):
`echo $LD_LIBRARY_PATH`
> /home/$USER@edu.local/miniconda3/envs/tf/lib/:/home/$USER@edu.local/miniconda3/envs/tf/lib/python3.9/site-packages/nvidia/cudnn/lib:/home/$USER@edu.local/miniconda3/envs/tf/lib/:/home/$USER@edu.local/miniconda3/envs/tf/lib/python3.9/site-packages/nvidia/cudnn/lib
`echo $CUDNN_PATH`
> /home/$USER@edu.local/miniconda3/envs/tf/lib/python3.9/site-packages/nvidia/cudnn
Damit wir diese Variabeln nicht in jeder Terminalsession erneut setzen müssen, können diese mit den Befehlen
```
mkdir -p $CONDA_PREFIX/etc/conda/activate.d
echo 'CUDNN_PATH=$(dirname $(python -c "import nvidia.cudnn;print(nvidia.cudnn.__file__)"))' >> $CONDA_PREFIX/etc/conda/activate.d/env_vars.sh
echo 'LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$CONDA_PREFIX/lib/:$CUDNN_PATH/lib' >> $CONDA_PREFIX/etc/conda/activate.d/env_vars.sh
echo 'export LD_LIBRARY_PATH=${LD_LIBRARY_PATH#:}' >> $CONDA_PREFIX/etc/conda/activate.d/env_vars.sh
```
permanent in der Conda Environment gespeichert werden.
Mit pip installieren wir nun Tensorflow:
```
pip install --upgrade pip
pip install tensorflow==2.12.*
python3 -c "import tensorflow as tf; print(tf.config.list_physical_devices('GPU'))"
```
Wenn alles richtig funktioniert, gibt der letzte Befehl Informationen zu den GPUs aus. Dies sieht, abhängig von der verbauten Hardware, etwa so aus:
| ![Nvidia SMI Output](./images/tensorflow-python-test.png)
|:--:
| Korrekt erkannte GPUs in Python
## Troubleshooting
Auf den Workstations ist eine Quota von 80GB pro Person und Homeverzeichnis gesetzt. Beim erstellen von Apptainerimages, ist es möglich, dass diese Quota überschritten wird und es folglich zu Abbrüchen und Fehlermeldungen kommt.
Mit dem Befehl `quota -s` kann angezeigt werden, wieviel Speicherplatz auf dem Homeverzeichnis übrig ist. Falls die Quota aufgebraucht ist, kann mit dem Befehl
```
apptainer cache clean -D 30
```
Apptainerimages die älter als 30 Tage sind gelöscht werden.
Falls immer noch zuwenig Speicherplatz im Homeverzeichnis vorhanden ist, kann mit dem Befehl
```
du -sh .[^..]* * | sort -h
```
herausgefunden werden in welchem Unterzeichnis die meisten Daten belegt werden.
Daten die im Verzeichnis **/scratch** abgelegt werden, unterliegen übrigens keiner Quota.
### Tensorflow Performance analysieren
https://www.tensorflow.org/guide/gpu_performance_analysis

60
Lab-Login-Tutorial.md Normal file
View File

@ -0,0 +1,60 @@
# Anleitung zum Einloggen ins CDS-Lab
*Diese Anleitung gilt für Windows 11. Anleitungen für Linux und MacOS werden mit der Zeit dazukommen.*
1. Falls nicht schon auf dem System vorhanden, die Windows PowerShell installieren https://www.microsoft.com/store/productId/9MZ1SNWT0N5D
2. Sich via PulseSecure mit dem VPN der FHGR verbinden.
3. PowerShell starten.
4. Folgenden Befehl eingeben:
```
ssh benutzername@maschine.fhgr.ch
```
**Zuteilung der Maschinen nach Jahrgang der Studierenden**
- 2021: mercury
- 2022: nickel
- 2023: palladium
![Login Schritt 1](./images/lab-login-1.png)
5. Falls der gewählte host, in diesem Beispiel `palladium.fhgr.ch` noch nicht auf der Liste der von eurem Computer bekannten hosts ist, wird er jetzt hinzugefügt. Es erscheint folgende Nachricht, bei der ihr **«yes»** eintippen sollt:
![Login Schritt 2](./images/lab-login-2.png)
6. Danach erscheint folgende "Warnung", dh. ihr habt alles richtig gemacht, und es wird nach eurem Passwort gefragt.
![Login Schritt 3](./images/lab-login-3.png)
7. Gebt nun euer FHGR-Passwort ein.
8. Falls ihr euch zum ersten Mail ins System einloggt, wird nun euer home directory erstellt:
![Login Schritt 4](./images/lab-login-4.png)
9. Ansonsten erscheint direkt die folgende Meldung:
![Login Schritt 5](./images/lab-login-5.png)
10. Falls das System ein Update benötigt, erscheint folgende (oder ähnliche) Nachricht:
![Login Schritt 6](./images/lab-login-6.png)
Ihr müsst **nichts** unternehmen, da die Updates jeweils zentral von der IT getätigt werden!
11. Falls kein Update nötig ist, seht ihr direkt folgende boilerplate Nachricht, ansonsten follgt diese nach der obigen Update-Nachricht:
![Login Schritt 7](./images/lab-login-7.png)
12. Darunter steht euer Benutzername, eingeloggt auf der gewählten Maschine. Im Beispiel sieht das so aus:
![Login Schritt 8](./images/lab-login-8.png)
13. Ihr seid nun bereit zur Nutzung des CDS-Labs.
## Fragen?
Wendet euch bitte an den Systemadministrator (<thomas.keller@fhgr.ch>)

View File

@ -1,3 +1,18 @@
# infrastruktur-dok
## Hardwareübersicht Studiengang CDS
Dokumentation der CDS Studiengang IT-Infrastruktur
| Typ | Hostname | Berechnungsart | Zugriff | Bemerkung
| ------------- |----------------| ----------| ----| ---------
| Workstation | mercury.fhgr.ch | GPU bound | ssh mit FHGR Credentials|
| Workstation | nickel.fhgr.ch | GPU bound | ssh mit FHGR Credentials|
| Workstation | palladium.fhgr.ch| GPU bound | ssh mit FHGR Credentials|
| Cluster | lithium.fhgr.ch | CPU bound | ssh mit FHGR Credentials |
### Ausgemustert
| Typ | Hostname | Berechnungsart | Zugriff | Bemerkung
| ------------- |----------------| ----------| ----| ---------
| Workstation | krypton.fhgr.ch | GPU bound | ssh mit FHGR Credentials | Ausser Betrieb
| Workstation | helium.fhgr.ch | GPU bound | ssh mit FHGR Credentials | Ausser Betrieb
| Workstation | iridium.fhgr.ch | GPU bound | ssh mit FHGR Credentials | Ausser Betrieb

67
Workstations-CDS.md Normal file
View File

@ -0,0 +1,67 @@
# CDS Workstations
## Übersicht
Der Studiengang CDS stellt seinen Studierenden folgende IT-Infrastruktur zur Verfügung:
| Typ | Hostname | Berechnungsart | Zugriff | Bemerkung
| ------------- |----------------| ----------| ----| ---------
| Workstation | mercury.fhgr.ch | GPU bound | ssh mit FHGR Credentials|
| Workstation | nickel.fhgr.ch | GPU bound | ssh mit FHGR Credentials|
| Workstation | helium.fhgr.ch | GPU bound | ssh mit FHGR Credentials | Abschaltung per 30.6.23
| Workstation | lithium.fhgr.ch | GPU bound | ssh mit FHGR Credentials | Abschaltung per 30.6.23
| Workstation | krypton.fhgr.ch | GPU bound | ssh mit FHGR Credentials | Abschaltung per 30.6.23
## Softwareinstalltionen auf den Workstations
Da es sich bei allen Rechnern um **Mehrbenutzersysteme** handelt, ist der Zugriff mit **su oder sudo auf den Workstations abgeschaltet**. Zusätzliche Software für Berechnungen, wie Tensorflow, können jedoch im Home des Benutzers installiert werden und zwar mit
- Miniconda
- Apptainer
*Hinweis: Docker ist auf den HPC Workstations nicht verfügbar. Dockercontainer können jedoch mit Apptainer ausgeführt werden*
## Installation mit Miniconda
```
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
bash Miniconda3-latest-Linux-x86_64.sh
rm Miniconda3-latest-Linux-x86_64.sh
```
Neue Virtuelle Umgebung erstellen und aktivieren
```
coda create --name <ENVNAME>
conda activate <ENVNAME>
```
Virtuelle Umgebungen anzeigen: `conda env list`
Software suchen und installieren
```
conda search <PKGNAME>
conda install <PKGNAME>
```
Upate von Python:
```conda update python3```
Update aller Pakete:
```conda update --all```
Update von Conda:
```conda update -n base -c defaults conda```
## Apptainer
Apptainer (ehemals Singularity) ist ein *Containersystem* das für den Einsatz auf HPC Systemen optimiert ist. Apptainer unterstützt verschiedene Clustertechnologien wie Infinband, SLURM und MPI. Da Apptainer den OCI Standard unterstützt, können neben Apptainerimages auch Container von Dockerhub oder Nvidia (https://catalog.ngc.nvidia.com/containers) unter Apptainer ausgeführt werden.
Auf unsren Workstations kann damit in einem Container eine Runtime für GPU und CPU Berechnungen mit Frameworks wie Tensorflow, Conda etc. ausgeführt werden.
## GPU Berechnungen mit Apptainer
Damit Tensorflow die GPUs auf den Workstations zur Berechnung nutzt, müssen verschiedene Komponenten im Container und der Workstation vorhanden sein. Grundsätzlich sind dies:

BIN
images/cluster/Cluster.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

View File

@ -0,0 +1,298 @@
<mxfile host="app.diagrams.net" modified="2023-09-05T14:24:09.558Z" agent="Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/117.0" etag="ohOYGRx74R8A3_-iqdB2" version="21.7.2" type="device">
<diagram name="Page-1" id="okKH00xu-Q76sZOU3yRa">
<mxGraphModel dx="1400" dy="2062" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="-ApnyZZP5jAH9naTh88D-63" value="" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.875;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;exitX=0.16;exitY=0.55;exitDx=0;exitDy=0;exitPerimeter=0;curved=1;endArrow=none;endFill=0;strokeWidth=6;fontSize=16;fontStyle=0" edge="1" parent="1" source="-ApnyZZP5jAH9naTh88D-61" target="-ApnyZZP5jAH9naTh88D-22">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="1008" y="-121" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-73" value="&lt;font style=&quot;font-size: 18px;&quot;&gt;&lt;b style=&quot;font-size: 18px;&quot;&gt;SSH&lt;/b&gt;&lt;/font&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=18;labelBackgroundColor=none;" vertex="1" connectable="0" parent="-ApnyZZP5jAH9naTh88D-63">
<mxGeometry x="-0.568" y="4" relative="1" as="geometry">
<mxPoint x="-1" y="18" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-25" value="" style="rounded=1;whiteSpace=wrap;html=1;strokeWidth=9;fillColor=#CCCCCC;fontColor=#333333;strokeColor=#333333;glass=0;shadow=0;gradientColor=#666666;labelPosition=center;verticalLabelPosition=middle;align=center;verticalAlign=middle;fontSize=19;" vertex="1" parent="1">
<mxGeometry x="340" y="230" width="1270" height="620" as="geometry" />
</mxCell>
<mxCell id="DeRy2OjbTxsYnWNSGUsd-11" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=3;endArrow=none;endFill=0;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="537" y="428" />
<mxPoint x="537" y="428" />
</Array>
<mxPoint x="537" y="468" as="sourcePoint" />
<mxPoint x="537" y="414.5" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="DeRy2OjbTxsYnWNSGUsd-17" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=10;startArrow=none;startFill=0;endArrow=none;endFill=0;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="537" y="562" />
<mxPoint x="537" y="562" />
</Array>
<mxPoint x="537" y="508" as="sourcePoint" />
<mxPoint x="537" y="587" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="DeRy2OjbTxsYnWNSGUsd-12" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=3;endArrow=none;endFill=0;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="757" y="408" />
<mxPoint x="757" y="408" />
</Array>
<mxPoint x="757" y="468" as="sourcePoint" />
<mxPoint x="757" y="414.1666666666665" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="DeRy2OjbTxsYnWNSGUsd-18" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=10;endArrow=none;endFill=0;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="757" y="562" />
<mxPoint x="757" y="562" />
</Array>
<mxPoint x="757" y="508" as="sourcePoint" />
<mxPoint x="757" y="587" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="DeRy2OjbTxsYnWNSGUsd-13" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=3;endArrow=none;endFill=0;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="978" y="468" as="sourcePoint" />
<mxPoint x="979" y="414.5" as="targetPoint" />
<Array as="points">
<mxPoint x="979" y="468" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="DeRy2OjbTxsYnWNSGUsd-19" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=10;endArrow=none;endFill=0;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="978" y="559" />
<mxPoint x="978" y="559" />
</Array>
<mxPoint x="978" y="505" as="sourcePoint" />
<mxPoint x="978" y="586.5" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="DeRy2OjbTxsYnWNSGUsd-15" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=3;endArrow=none;endFill=0;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="1405" y="428" />
<mxPoint x="1405" y="428" />
</Array>
<mxPoint x="1405" y="468" as="sourcePoint" />
<mxPoint x="1405" y="414.5" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="DeRy2OjbTxsYnWNSGUsd-21" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=10;endArrow=none;endFill=0;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="1405" y="572" />
<mxPoint x="1405" y="572" />
</Array>
<mxPoint x="1405" y="508" as="sourcePoint" />
<mxPoint x="1405" y="587" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="DeRy2OjbTxsYnWNSGUsd-14" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=3;endArrow=none;endFill=0;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="1195" y="418" />
<mxPoint x="1195" y="418" />
</Array>
<mxPoint x="1195" y="468" as="sourcePoint" />
<mxPoint x="1195" y="414.83333333333326" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="DeRy2OjbTxsYnWNSGUsd-20" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=10;endArrow=none;endFill=0;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="1195.5" y="574" />
<mxPoint x="1195.5" y="574" />
</Array>
<mxPoint x="1195.5" y="510" as="sourcePoint" />
<mxPoint x="1195.5" y="589" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="DeRy2OjbTxsYnWNSGUsd-8" value="&lt;font style=&quot;font-size: 18px;&quot;&gt;&lt;b&gt;&lt;font style=&quot;font-size: 18px;&quot;&gt;Ethernet&lt;/font&gt;&amp;nbsp;&amp;nbsp; &lt;/b&gt;&lt;/font&gt;" style="line;strokeWidth=7;html=1;perimeter=backbonePerimeter;points=[];outlineConnect=0;labelPosition=left;verticalLabelPosition=middle;align=right;verticalAlign=middle;fontSize=24;fillColor=default;labelBackgroundColor=none;fontColor=#1A1A1A;" parent="1" vertex="1">
<mxGeometry x="440" y="407" width="1071" height="10" as="geometry" />
</mxCell>
<mxCell id="DeRy2OjbTxsYnWNSGUsd-16" value="&lt;div style=&quot;font-size: 19px;&quot; align=&quot;right&quot;&gt;&lt;font style=&quot;font-size: 19px;&quot;&gt;&lt;b style=&quot;font-size: 18px;&quot;&gt;&lt;font style=&quot;font-size: 18px;&quot;&gt;Infiniband&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;br&gt;&lt;/font&gt;&lt;/b&gt;&lt;/font&gt;&lt;/div&gt;" style="line;strokeWidth=15;html=1;perimeter=backbonePerimeter;points=[];outlineConnect=0;horizontal=1;labelBackgroundColor=none;labelPosition=left;verticalLabelPosition=middle;align=right;verticalAlign=middle;gradientColor=none;gradientDirection=west;fontColor=#1A1A1A;" parent="1" vertex="1">
<mxGeometry x="453" y="588" width="1059" height="10" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-3" value="" style="image;html=1;image=img/lib/clip_art/computers/Server_128x128.png;labelBackgroundColor=none;fontColor=#333333;fontStyle=1;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="469" y="451" width="128" height="64" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-4" value="" style="image;html=1;image=img/lib/clip_art/computers/Server_128x128.png;labelBackgroundColor=none;fontColor=#FF0000;fontStyle=1;labelPosition=left;verticalLabelPosition=bottom;align=right;verticalAlign=top;fontSize=18;" vertex="1" parent="1">
<mxGeometry x="693" y="451" width="128" height="64" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-5" value="" style="image;html=1;image=img/lib/clip_art/computers/Server_128x128.png;labelBackgroundColor=none;fontColor=#FF0000;fontStyle=1;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;fontSize=18;" vertex="1" parent="1">
<mxGeometry x="912" y="451" width="128" height="64" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-6" value="" style="image;html=1;image=img/lib/clip_art/computers/Server_128x128.png;labelBackgroundColor=none;fontColor=#FF0000;fontStyle=1;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;fontSize=18;" vertex="1" parent="1">
<mxGeometry x="1129" y="451" width="128" height="64" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-7" value="" style="image;html=1;image=img/lib/clip_art/computers/Server_128x128.png;labelBackgroundColor=none;fontColor=#FF0000;fontStyle=1;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;fontSize=18;" vertex="1" parent="1">
<mxGeometry x="1340" y="451" width="128" height="64" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-97" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;strokeWidth=15;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="709" y="788.3333333333335" as="sourcePoint" />
<mxPoint x="1229" y="789.3333333333335" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-102" value="Infiniband" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;fontSize=20;fontColor=#1A1A1A;fontStyle=1" vertex="1" connectable="0" parent="-ApnyZZP5jAH9naTh88D-97">
<mxGeometry x="0.1466" y="-2" relative="1" as="geometry">
<mxPoint x="-24" y="18" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-99" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;strokeWidth=15;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="790" y="788" as="sourcePoint" />
<mxPoint x="639" y="600" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-20" value="" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=7;endArrow=none;endFill=0;fontSize=17;fontColor=#1A1A1A;curved=1;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="835.5686725453247" y="-36" as="sourcePoint" />
<mxPoint x="979" y="213" as="targetPoint" />
<Array as="points">
<mxPoint x="979" y="-36" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-30" value="SSH" style="edgeLabel;html=1;align=center;verticalAlign=top;resizable=0;points=[];fontSize=18;labelPosition=center;verticalLabelPosition=bottom;labelBackgroundColor=none;fontStyle=1" vertex="1" connectable="0" parent="-ApnyZZP5jAH9naTh88D-20">
<mxGeometry x="0.36" y="2" relative="1" as="geometry">
<mxPoint x="-30" y="53" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-8" value="&lt;font color=&quot;#333333&quot;&gt;Masternode&lt;/font&gt;" style="image;html=1;image=img/lib/clip_art/computers/Server_128x128.png;labelBackgroundColor=none;fontColor=#FF0000;fontStyle=1;labelPosition=right;verticalLabelPosition=top;align=left;verticalAlign=bottom;strokeWidth=5;imageBorder=none;fontSize=18;" vertex="1" parent="1">
<mxGeometry x="916" y="212" width="128" height="64" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-29" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=7;endArrow=none;endFill=0;fontSize=15;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="978" y="408.5" as="sourcePoint" />
<mxPoint x="978" y="273" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-15" value="&lt;font color=&quot;#003366&quot; style=&quot;font-size: 18px;&quot;&gt;Ethernet Switch&lt;/font&gt;" style="image;html=1;image=img/lib/clip_art/networking/Switch_128x128.png;labelBackgroundColor=none;fontColor=#0000FF;fontSize=18;fontStyle=1;labelPosition=right;verticalLabelPosition=middle;align=left;verticalAlign=middle;imageBorder=none;imageBackground=none;" vertex="1" parent="1">
<mxGeometry x="930.5" y="300" width="99" height="90" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-22" value="&lt;font size=&quot;1&quot;&gt;&lt;b style=&quot;font-size: 26px;&quot;&gt;Fachhochschulnetz&lt;/b&gt;&lt;/font&gt;" style="ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#0050ef;strokeColor=#001DBC;strokeWidth=9;fontColor=#ffffff;gradientColor=default;" vertex="1" parent="1">
<mxGeometry x="430" y="-270" width="706" height="420" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-31" value="Clusternetz" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=26;fontColor=#4D4D4D;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="430" y="260" width="140" height="70" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-33" value="" style="image;html=1;image=img/lib/clip_art/computers/Netbook_128x128.png" vertex="1" parent="1">
<mxGeometry x="836" y="-30" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-34" value="" style="image;html=1;image=img/lib/clip_art/computers/Netbook_128x128.png" vertex="1" parent="1">
<mxGeometry x="690" y="-190" width="87" height="110" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-47" value="&lt;font color=&quot;#333333&quot; style=&quot;font-size: 15px;&quot;&gt;Computenode&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="891" y="514" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-48" value="&lt;font color=&quot;#333333&quot; style=&quot;font-size: 15px;&quot;&gt;Computenode&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="452" y="515" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-49" value="&lt;font color=&quot;#333333&quot; style=&quot;font-size: 15px;&quot;&gt;Computenode&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="1107" y="514" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-50" value="&lt;font color=&quot;#333333&quot; style=&quot;font-size: 15px;&quot;&gt;Computenode&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="673" y="513" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-52" value="&lt;font color=&quot;#333333&quot; style=&quot;font-size: 15px;&quot;&gt;Computenode&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="1316" y="514" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-61" value="&lt;font size=&quot;1&quot;&gt;&lt;b style=&quot;font-size: 27px;&quot;&gt;VPN&lt;/b&gt;&lt;/font&gt;" style="ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#0050ef;strokeColor=#001DBC;strokeWidth=9;fontColor=#ffffff;gradientColor=default;" vertex="1" parent="1">
<mxGeometry x="1143" y="-270" width="359" height="270" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-62" value="" style="image;html=1;image=img/lib/clip_art/computers/Netbook_128x128.png" vertex="1" parent="1">
<mxGeometry x="1289" y="-125" width="87" height="110" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-79" value="BeeGFS" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;strokeWidth=7;labelPosition=center;verticalLabelPosition=top;align=center;verticalAlign=bottom;labelBackgroundColor=none;horizontal=0;fontSize=14;fontStyle=1;fontColor=#333333;" edge="1" parent="1">
<mxGeometry x="-0.3244" y="-25" relative="1" as="geometry">
<mxPoint x="1195" y="674" as="sourcePoint" />
<mxPoint x="1195" y="597" as="targetPoint" />
<Array as="points">
<mxPoint x="1195" y="597" />
</Array>
<mxPoint y="1" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-80" value="JBOD" style="strokeWidth=2;html=1;shape=mxgraph.flowchart.database;whiteSpace=wrap;gradientColor=default;verticalAlign=top;labelPosition=center;verticalLabelPosition=bottom;align=center;fontSize=15;fontStyle=1;fontColor=#333333;strokeColor=#333333;fillColor=#B3B3B3;" vertex="1" parent="1">
<mxGeometry x="1167.5" y="675" width="56" height="40" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-81" value="BeeGFS" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;strokeWidth=7;labelPosition=center;verticalLabelPosition=top;align=center;verticalAlign=bottom;labelBackgroundColor=none;horizontal=0;fontSize=14;fontStyle=1;fontColor=#333333;" edge="1" parent="1">
<mxGeometry x="-0.3244" y="-25" relative="1" as="geometry">
<mxPoint x="757" y="674" as="sourcePoint" />
<mxPoint x="757" y="597" as="targetPoint" />
<Array as="points">
<mxPoint x="757" y="597" />
</Array>
<mxPoint y="1" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-82" value="JBOD" style="strokeWidth=2;html=1;shape=mxgraph.flowchart.database;whiteSpace=wrap;gradientColor=default;verticalAlign=top;labelPosition=center;verticalLabelPosition=bottom;align=center;fontSize=15;fontStyle=1;fontColor=#333333;strokeColor=#333333;fillColor=#B3B3B3;" vertex="1" parent="1">
<mxGeometry x="729.5" y="675" width="56" height="40" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-83" value="BeeGFS" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;strokeWidth=7;labelPosition=center;verticalLabelPosition=top;align=center;verticalAlign=bottom;labelBackgroundColor=none;horizontal=0;fontSize=14;fontStyle=1;fontColor=#333333;" edge="1" parent="1">
<mxGeometry x="-0.3244" y="-25" relative="1" as="geometry">
<mxPoint x="537" y="674" as="sourcePoint" />
<mxPoint x="537" y="597" as="targetPoint" />
<Array as="points">
<mxPoint x="537" y="597" />
</Array>
<mxPoint y="1" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-84" value="JBOD" style="strokeWidth=2;html=1;shape=mxgraph.flowchart.database;whiteSpace=wrap;gradientColor=default;verticalAlign=top;labelPosition=center;verticalLabelPosition=bottom;align=center;fontSize=15;fontStyle=1;fontColor=#333333;strokeColor=#333333;fillColor=#B3B3B3;" vertex="1" parent="1">
<mxGeometry x="509.5" y="675" width="56" height="40" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-86" value="JBOD" style="strokeWidth=2;html=1;shape=mxgraph.flowchart.database;whiteSpace=wrap;gradientColor=default;verticalAlign=top;labelPosition=center;verticalLabelPosition=bottom;align=center;fontSize=15;fontStyle=1;fontColor=#333333;strokeColor=#333333;fillColor=#B3B3B3;" vertex="1" parent="1">
<mxGeometry x="1377.5" y="675" width="56" height="40" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-87" value="BeeGFS" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;strokeWidth=7;labelPosition=center;verticalLabelPosition=top;align=center;verticalAlign=bottom;labelBackgroundColor=none;horizontal=0;fontSize=14;fontStyle=1;fontColor=#333333;" edge="1" parent="1">
<mxGeometry x="-0.3244" y="-25" relative="1" as="geometry">
<mxPoint x="1405" y="674" as="sourcePoint" />
<mxPoint x="1405" y="597" as="targetPoint" />
<Array as="points">
<mxPoint x="1405" y="597" />
</Array>
<mxPoint y="1" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-89" value="BeeGFS" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;strokeWidth=7;labelPosition=center;verticalLabelPosition=top;align=center;verticalAlign=bottom;labelBackgroundColor=none;horizontal=0;fontSize=14;fontStyle=1;fontColor=#333333;" edge="1" parent="1">
<mxGeometry x="-0.3244" y="-25" relative="1" as="geometry">
<mxPoint x="978" y="674" as="sourcePoint" />
<mxPoint x="978" y="597" as="targetPoint" />
<Array as="points">
<mxPoint x="978" y="597" />
</Array>
<mxPoint y="1" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-90" value="JBOD" style="strokeWidth=2;html=1;shape=mxgraph.flowchart.database;whiteSpace=wrap;gradientColor=default;verticalAlign=top;labelPosition=center;verticalLabelPosition=bottom;align=center;fontSize=15;fontStyle=1;fontColor=#333333;strokeColor=#333333;fillColor=#B3B3B3;" vertex="1" parent="1">
<mxGeometry x="950.5" y="675" width="56" height="40" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-101" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;strokeWidth=15;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1162" y="789.0666666666671" as="sourcePoint" />
<mxPoint x="1301.5333333333333" y="598" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-96" value="Infiniband Switch" style="image;html=1;image=img/lib/clip_art/networking/Switch_128x128.png;labelBackgroundColor=none;fontColor=#003366;fontSize=18;fontStyle=1;aspect=fixed;" vertex="1" parent="1">
<mxGeometry x="1229" y="721.34" width="141" height="140" as="geometry" />
</mxCell>
<mxCell id="-ApnyZZP5jAH9naTh88D-103" value="Infiniband Switch" style="image;html=1;image=img/lib/clip_art/networking/Switch_128x128.png;labelBackgroundColor=none;fontColor=#003366;fontSize=18;fontStyle=1;aspect=fixed;" vertex="1" parent="1">
<mxGeometry x="570" y="721.34" width="141" height="140" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

BIN
images/lab-login-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
images/lab-login-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
images/lab-login-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
images/lab-login-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
images/lab-login-5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
images/lab-login-6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
images/lab-login-7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
images/lab-login-8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB