W tym dokumencie opisano proces tworzenia usług NLP (nlpworkers).
Tworzenie usług NLP w ramach architektury CLARIN polega na stworzeniu klasy dziedziczącej i zdefiniowaniu kilku metod, które były abstrakcyjne. Najważniejszą z nich jest funkcja process
, która przyjmuje trzy argumenty: ścieżkę do pliku wejściowego, opcje (klucz-wartość) i ścieżka do pliku wyjściowego (programista jest odpowiedzialny za stworzenie pliku o przekazanej ścieżce). Standardowo pliki wejściowe i wyjściowe znajdują się na dysku sieciowym (aktualnie używamy prot. SMB - katalog jest najczęściej podmontowywany w /samba). Parametry z którymi ma zostać wywołana funkcja process
są przesyłane za pośrednictwem systemu kolejkowego Rabbit (jest on również używany do łączenia usług w potoki przetwarzania). Każda usługa NLP powinna mieć swoje repozytorium w https://gitlab.clarin-pl.eu/nlpworkers.
Aktualnie istnieją trzy wersje wspomnianych bibliotek (zawierających wspomnianą wyżej klasę abstrakcyjną): dla języka Python, Java i C++. Niżej opisano proces tworzenia usługi w każdym z wymienionych języków.
Aby stworzyć usługę NLP w języku Python niezbędne jest zainstalowanie paczki nlp_ws (jest ona dostępna z poziomu naszego serwera PYPI: https://pypi.clarin-pl.eu). Aby ją zainstalować należy wprowadzić komendę (najlepiej w wirtualnym środowisku) pip install --index-url https://pypi.clarin-pl.eu/simple/ nlp_ws
(lepszym rozwiązaniem jest umieszczenie nazwy biblioteki wraz z innymi używanymi bibliotekami w pliku requirements.txt
i użycie komendy pip install --index-url https://pypi.clarin-pl.eu/simple/ -r requirements.txt
).
Niżej przedstawiono prosty przykład usługi, która kopiuje pliki z jednego katalogu do drugiego:
"""Implementetion of dummy worker."""
import nlp_ws
class Worker(nlp_ws.NLPWorker):
"""Class that implements example worker."""
def process(self, input_file: str, task_options: dict, output_file: str) -> None:
"""Implementation of example tasks that copies files."""
shutil.copy(input_file, output_file)
if __name__ == '__main__':
nlp_ws.NLPService.main(Worker)
Jak wspomniano wyżej, najważniejszą funkcją, którą trzeba zdefiniować, jest funkcja process
- w powyższym przykładzie zajmuje się ona kopiowaniem plików z katalogu input_file
do output_file
(plik wyjściowy jest tworzony przez komendę shutil.copy
). Klasa nlp_ws.NLPWorker
posiada więcej przydanych funkcji (funkcje są dobrze udokumentowane w kodzie biblioteki https://gitlab.clarin-pl.eu/nlpworkers/nlp_ws:
static_init
- funkcja, która pozwala na inicjalizację współdzielonych zasobów - wszystko co przypiszemy do zmiennej cls
zostanie spakowane (pickle) i wysłane do każdego procesu (funkcje process są uruchamiane w osobnych procesach)init
- inicjalizacja każdego Workera (funkcja działa w sposób zbliżony do standardowego konstruktora initclose
- funkcja pozwalajaca na zwolnienie zarezerwowanych zasobów przez danego Workera (w związku z istnieniem garbage collectora jest ona rzadko używana src/ - źródła aplikacji
tests/ - testy
main.py - skrypt wejściowy
config.ini - plik konfuguracyjny (opisany niżej)
Dockerfile - definicja budowanie obrazu Docker
.gitlab-ci.yml - definicja potoku CI
.gitignore - pliki mające być ignorowane przez system git
tox.ini - konfiguracja narzędzia tox
requirements.txt - wymagane biblioteki
Aby stworzyć usługę NLP w języku JAVA niezbędne jest użycie artefaktu nlp.worker. Aby go zainstalować należy do pliku pom.xml
w sekcji <dependencies>
dodać zależność:
<dependency>
<groupId>pl.clarin</groupId>
<artifactId>nlp.worker</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
Poniżej przykład prostej usługi, która kopiuje pliki z jednego katalogu do drugiego:
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.json.JSONObject;
import pl.clarin.ws.worker.IniFile;
import pl.clarin.ws.worker.Service;
import pl.clarin.ws.worker.Worker;
public class Dummy extends Worker {
@Override
public void process(String fileIn, String fileOut, JSONObject param) throws IOException {
Path sourcePath = Paths.get(fileIn);
Path destinationPath = Paths.get(fileOut);
try {
Files.copy(sourcePath, destinationPath,
StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
Logger.getLogger(Service.class.getName()).log(Level.ERROR, "Problems in reading/writing for file :" + fileIn, e);
}
}
public static void main(String[] args) {
new Service<Dummy>(Dummy.class);
}
}
Najważniejszą funkcją, którą trzeba zdefiniować, jest funkcja process
- w powyższym przykładzie zajmuje się ona kopiowaniem plików z katalogu fileIn
do fileOut
. Klasa pl.clarin.ws.worker.Worker
posiada więcej przydanych funkcji (funkcje są udokumentowane w kodzie biblioteki https://gitlab.clarin-pl.eu/nlpworkers/nlp.worker)
module/ - źródła aplikacji i zależności
module/pom.xml - plik pom.xml zawierający zależności
module/src/ - źródła aplikacji
module/src/main/java/pl/clarin/<nazwa projektu>/ - pliki źródłowe
module/src/test/java/ - testy
config.ini - plik konfuguracyjny (opisany niżej)
Dockerfile - definicja budowanie obrazu Docker
.gitlab-ci.yml - definicja potoku CI
.gitignore - pliki mające być ignorowane przez system git
Każda usługa powinna zawierać plik konfiguracyjny config.ini
. Jego podstawowa wersja wygląda w następujący sposób:
[service]
tool = nazwa_uslugi
root = /samba/requests/
rabbit_host = addr
rabbit_user = test
rabbit_password = test
[tool]
workers_number = 1
[logging]
port = 9981
local_log_level = INFO
Plik ten zawiera informacje nazwie usługi (pod taką nazwą usługa będzie widoczna w systemie), ścieżce z plikami wejściowymi i wyjściowymi, login i hasło pozwalające połączyć się z systemem Rabbit i liczbę procesów, które mają zostać uruchomione w ramach działania usługi (workers_number
) oraz czas (podany w godzinach) po jakim zadanie wygaśnie (expiry_hours
). W celu standaryzacji usług zaleca się umieszczanie własnych zmiennych w sekcji tool
. Na przykład:
[tool]
arg_input_prefix = /samba
Możliwe jest zdefiniowanie zmiennych środowiskowych które nadpiszą zmienne wprowadzone za pomocą pliku config.ini
a także całkowite zrezygnowanie z pliku config.ini
i zdefiniowanie wszystkich wymaganych zmiennych jako zmienne środowiskowe.
Zmienna powinna się rozpoczynać od opisu sekcji, jednego z poniższych:
CFG_S_SERV - odpowiada sekcji [service]
CFG_S_TOOL - odpowiada sekcji [tool]
CFG_S_LOG - odpowiada sekcji [logging]
CFG_S_LOGLEVELS - odpowiada sekcji [logging_levels]
Dalsza część zmiennej to _OPT_NAZWA_OPCJI, predefiniowane opcje:
Dla sekcji service:
CFG_S_SERV_OPT_ROOT
CFG_S_SERV_OPT_TOOL
CFG_S_SERV_OPT_QUEUE_PREFIX
CFG_S_SERV_OPT_RABBIT_HOST
CFG_S_SERV_OPT_RABBIT_USER
CFG_S_SERV_OPT_RABBIT_PASSWORD
CFG_S_SERV_OPT_IS_PRODUCTION
Dla sekcji tool:
CFG_S_TOOL_OPT_WORKERS_NUMBER
Dla sekcji log:
CFG_S_LOG_OPT_PORT
CFG_S_LOG_OPT_LOGFILE_NAME
CFG_S_LOG_OPT_LOGFILE_MAXSIZE
CFG_S_LOG_OPT_LOGFILE_MAXBACKUPS
CFG_S_LOG_OPT_LOG_FORMAT
CFG_S_LOG_OPT_LOCAL_LOG_LEVEL
Dostępne jest także definiowanie własnych zmiennych, takie zmienne powinny mieć strukture taką jak zmienne predefiniowane. Przykładowo zmienna o nazwie my_awesome_variable w sekcji service:
CFG_S_SERV_OPT_MY_AWESOME_VARIABLE
config.ini
):CFG_S_SERV_OPT_ROOT
CFG_S_SERV_OPT_TOOL
CFG_S_SERV_OPT_RABBIT_HOST
CFG_S_SERV_OPT_RABBIT_USER
CFG_S_SERV_OPT_RABBIT_PASSWORD
Każda usługa powinna posiadać plik Dockerfile, który pozwala na budowę obrazu Docker. Plik ten powinien być budowany na podstawie jednego z predefiniowanych obrazów (posiadają one informacje między innymi o naszym serwerze PYPI, czy APT (instalacja narzędzi nie wymaga żadnych specjalnych akcji). Lista obrazów bazowych jest dostępna pod adresem https://gitlab.clarin-pl.eu/dockers
Niżej zamieszczono prosty przykład pliku Dockerfile (obraz clarinpl/cuda-python:3.6 pozwala na używanie karty graficznej - jeżeli ta funkcja jest zbędna należy użyć innego obrazu)
FROM clarinpl/cuda-python:3.6
WORKDIR /home/worker
COPY ./src ./src
COPY ./cmc_service.py .
COPY ./requirements.txt .
COPY ./models/CMC ./models/CMC
COPY ./entrypoint.sh .
RUN apt update && apt install -y g++ gdb
RUN git clone https://github.com/facebookresearch/fastText.git && \
cd fastText && \
python3.6 -m pip install . && \
cd .. && \
rm -rf fastText
RUN python3.6 -m pip install -r requirements.txt
RUN ["chmod", "+x", "./entrypoint.sh"]
CMD ["./entrypoint.sh"]
Gitlab pozwala na tworzenie prostych potoków CI/CD (https://docs.gitlab.com/ee/ci/). Każdy nowy projekt powinien zawierać taki plik, który uruchamia sprawdzanie jakości kodu (code style), uruchamia testy, budowanie i wysylanie obrazu Docker (ten krok powinien być uruchamiany jedynie na branchu master). Niżej przedstawiono prosty przykład CI:
image: clarinpl/python:3.6
cache:
paths:
- .tox
stages:
- check_style
- build
before_script:
- pip install tox==2.9.1
pep8:
stage: check_style
script:
- tox -v -e pep8
docstyle:
stage: check_style
script:
- tox -v -e docstyle
build_image:
stage: build
image: docker:18.09.7
only:
- master
services:
- docker:18.09.7-dind
before_script:
- ''
script:
- docker build -t clarinpl/websim .
- echo $DOCKER_PASSWORD > pass.txt
- cat pass.txt | docker login --username $DOCKER_USERNAME --password-stdin
- rm pass.txt
- docker push clarinpl/websim