====== Řízení pokusů pomocí Makefilů ====== **Danova úvodní poznámka:** V poslední době se mi několikrát stalo, že jsem potřeboval aplikovat stejnou řadu skriptů za několik různých datových souborů, případně stejný pokus zopakovat po delší době, takže bylo pracné si vzpomenout, jak přesně jsem co volal. Konkrétně šlo o vícejazyčné soutěže ve zpracování přirozených jazyků (CoNLL parsing, Morpho Challenge), a pak také o vyhodnocování úspěšnosti zápočtových úkolů. Nabízelo se řešení pomocí [[http://www.gnu.org/software/make/manual/make.html|make]] a Makefilů, kde lze posloupnost zpracování dat jednotlivými skripty dobře popsat. Současně jsem ale vždy znova zápasil s věcmi, které se mi v Makefilech řešily těžko. Tenhle dokument je tedy pokusem shrnout problémy a poznamenat si řešení, vlastně jakýsi makefile k Makefilům. ===== Výchozí předpoklady ===== Odněkud jsme získali výchozí data. Ta leží v jedné nebo několika složkách, kde je chceme nechat beze změny, nepoškodit je chybným spuštěním skriptu ani do těchto složek přidávat soubory, které nesouvisí se zdrojovými daty, ale spíše s jejich konkrétním zpracováním v rámci nějakého pokusu. Data tedy chceme nejdříve zkopírovat do pracovní složky. Typicky máme obdobná data pro několik různých jazyků. Je možné, že původní data (resp. jejich vydavatel) používají pojmenovávací konvenci, která nám nevyhovuje, takže je během kopírování chceme také přejmenovat. Je také možné, že na začátku potřebujeme provést nějaké operace, které nejsou pro všechny jazyky stejné, např. některé jazyky mohou používat nějaké exotické kódování a my chceme všechno sjednotit na UTF-8, nebo třeba chceme vytáhnout nějaké informace z morfologických značek, které mají v každém jazyce jinou strukturu. V rámci jednoho jazyka můžeme mít několik zdrojových souborů. Například trénovací data, vývojová data, závěrečná vyhodnocovací data atd. Je možné, že chceme nachystat paralelně dvě řady pokusů, kde v první řadě rozsekneme (svým vlastním algoritmem) oficiální trénovací data na trénovací a pokusná testovací, ve druhé pak použijeme celá oficiální trénovací data a otestujeme metodu na oficiálních testovacích. Kromě použitých vstupních dat bude zpracování v obou případech stejné. ===== Rozdělení na zdrojovou a pracovní složku ===== Je na uvážení, které kroky chceme dělat přímo ve zdrojové složce s daty a které až po zkopírování do pracovní složky. Sjednocení kódování na UTF-8 nebo převod dat do jiného formátu může být užitečné i pro úplně jiné pokusy, proto ho možná chceme provést ve zdrojové složce. Změna pojmenovávací konvence nebo vlastní rozdělení trénovacích dat na vývojová trénovací a vývojová testovací jsou příklady úprav, které leží na hranici. Můžeme je považovat za dostatečně obecně užitečné, abychom je chtěli dělat už ve zdrojové složce, nebo je můžeme odkázat až do pracovní složky. Trénování modelu, ukládání zpracovaných dat a hlášení vyhodnocovacího programu naopak typicky patří do pracovní složky. Standardní řetězec operací nad daty se nejlépe popisuje pomocí šablonových pravidel (např. jak vznikne soubor %.csts ze souboru %.conll). Bohužel proměnná část v takovém pravidle může být jen jedna. Může obsahovat i cestu, ale to se dá těžko použít pro zpracování spojené s kopírováním dat z jedné složky do jiné. Zdá se tedy, že je ideální rozdělit zpracování dat na dvě části (a zřejmě dva různě koncipované Makefily). V té první se provedou jazykově závislé operace (pro každý jazyk jiné explicitní pravidlo) a data se zkopírují ze zdrojové složky do pracovní. Druhou část tvoří již uniformní posloupnost operací s daty popsaná šablonovými pravidly. ===== Pomocné soubory a úklid ===== Většinu mezisouborů bychom měli současně uvést mezi cíli, aby nám je make nesmazal jako přechodné. Šetříme tím čas pro případ, že bude nutné zopakovat zpracování jen části souborů, a také činíme závislosti mezi soubory více explicitní (jinak se může stát, že změníme závislost přechodného souboru, který je smazán, a pro make to nebude dostatečný důvod, aby přepracoval cílový soubor). Je také dobré si nagenerovat cíle pro úklid zejména prázdných souborů, které vzniknou, když nějaký cíl volá příkaz, který píše na standardní výstup, standardní výstup je přesměrován do souboru a zpracování příkazu skončí nějakou chybou. Dá se také nastavit v Makefilu, aby dílčí cíle po chybě mazal. Jinak tam bude soubor, který nemá správný obsah, ale bude mít dost čerstvou časovou nálepku, takže ho make po odstranění chyby nebude chtít přepracovat. Pak je těžké v tom udržet pořádek. Pokud Makefile obsahuje zvláštní cíl ''**.SECONDARY**'' bez prerekvizit, znamená to, že make nemaže soubory, které považuje za přechodné. Pokud Makefile obsahuje zvláštní cíl ''**.DELETE_ON_ERROR**'' bez prerekvizit, make smaže cíle, které se změnily, ale některý příkaz v jejich pravidle skončil chybou. Je velmi vhodné tuto "volbu" zapnout. Většinu nástrojů volám tak, že píšou na ''STDOUT'', který mám přesměrovaný do souboru. Pokud nástroj skončí chybou, je většinou cílový soubor už vytvořen, byť má nulovou velikost. Má ale čerstvý údaj o čase vzniku, takže až chybu opravím a pustím make znova, make si bude myslet, že už tento soubor nemá předělávat. ===== Výpočty na clusteru ===== Některé kroky bývají výpočetně náročné a hodilo by se je dělat na clusteru. Zvláště pokud zpracováváme stejným způsobem třeba 10 jazyků, hodilo by se zpracovávat je všechny paralelně. Paralelní práce s makem je problém, protože je potřeba propojit hlídání závislostí s distribucí úloh: qsub se vrátí dříve než je cíl připraven, takže nemůžeme hned přejít k tvorbě cílů, které na něm závisí. GNU make umí paralelizovat zpracování v rámci několika procesorů téhož stroje (při volání můžeme říct, na kolik procesů se smí zpracování rozštěpit), ale podpora pro spolupráci s clusterem mu chybí. Existují nějaké paralelní maky, ale myslím, že nejsou standardně nainstalované všude (pravděpodobně ani u nás) a řešení na nich postavené by nebylo snadno přenositelné. ===== Absolutní a relativní cesty ===== Make neprovádí ''pwd'', takže neví, že ''$(MOJESLOZKA)/soubor.txt'' a ''soubor.txt'' je případně tentýž soubor (cíl). Má-li být součástí zpracování kopírování souborů z jedné složky do jiné, pak je nejbezpečnější uvádět všechny soubory absolutní cestou. V tom případě je ovšem potřeba počítat s tím, že make bude s dlouhou cestou k souboru pracovat po celou dobu včetně všech šablonových pravidel, což může případně komplikovat návrh šablon v těchto pravidlech. Nevýhodou je také délka. Kopie volání, které make vypíše do terminálu, budou méně přehledné. Pokud navíc pracujeme s velkým množstvím souborů, snadněji se nám stane, že někde překročíme nejvyšší povolenou délku příkazového řádku. Pokud oddělíme kopírování dat ze vzdálených složek do samostatného Makefilu a pokud v rámci jednoho Makefilu omezíme případné přesuny mezi složkami na takové, které se dají vyjádřit relativní cestou (tj. typicky podstrom složky, ve které leží Makefile), pak si vystačíme s relativními cestami. Poznámka: I kopírování ze vzdálených složek lze pak vyřešit tím, že si na vzdálené složky vyrobíme ze složky s Makefilem symbolický odkaz. ===== Rozměry ===== Soubory, které při zpracování vznikají, lze rozdělit podle následujících kritérií. Kritéria je vhodné zohlednit ve jménech souborů nebo složek, aby bylo možné zpracovávat skupinu souborů se stejným kritériem pomocí jednoho pravidla. Bohužel je často obtížné navrhnout optimální rozmístění kritérií v cestě k souboru, protože ''make'' umí v šablonových pravidlech pracovat pouze s jedním souvislým proměnným úsekem. * Fáze zpracování (train, trained-model, test-input, parsed...). Někdy je vyjádřená příponou souboru, protože řadě fází odpovídá konkrétní datový formát. Ale např. trénovací a testovací data bývají ve stejném formátu a bývá zvykem je rozlišit spíše ve jméně souboru než v příponě. Taky sem patří rozlišení testovacích dat na úplná (zahrnující i zlatý standard) a slepá (taková mají typicky k dispozici soutěžící před uzavřením soutěže). * Jazyk * Datová sada (dev vs. eval, popř. nějaká další, třeba out-of-domain data) * Velikost trénovacích dat pro křivku učení * Různá nastavení parseru a dalších nástrojů, různá předzpracování dat (např. převedení morfologických značek do sady PDT) ===== mdmake ===== Tohle jsou Danovy poznámky k budoucímu nástroji, který by měl řadu problémů odstranit tím, že z šablony ''makefile.mdm'' vygeneruje Makefile pro normální gnu make. MD-make znamená „multidimenzionální make“. * MD-makefile může obsahovat všechny syntaktické konstrukce, které obsahuje normální makefile. Konstrukce se opíší do vygenerovaného makefilu a za jejich zpracování bude zodpovědný až normální make. Je akorát nutné mít na paměti, že k jejich zpracování dojde po vygenerování makefilu. Pokud tedy např. vkládáme vnořené makefily pomocí include, musí to už být normální makefily, ne MD-makefily. * Vyjmenovat proměnné, které obsahují hodnoty v jednotlivých rozměrech. Současně dát najevo, jak se z nich poskládá jméno souboru. (Mezery se umažou, jsou tam proto, aby se poznalo, který oddělovač se má vynechat, když nějaký rozměr zcela chybí. Povolené oddělovače jsou lomítko, pomlčka a tečka.) .MDIMS: LANGUAGES/ DE TRAINTEST -PREPROCESSINGS .STATES * Oddělovače nejsou povinné, ale MD-make zkontroluje, zda kvůli chybějícím oddělovačům nemůžou vzniknout nejednoznačnosti (např. LANGUAGES = hi him, DOMAINS = mix ix, .MDIMS: LANGUAGES DOMAINS by způsobovalo problémy). * Poslední rozměr v seznamu rozměrů má výlučné postavení. Nemusí se jmenovat STATES a nemusí být oddělen právě tečkou (i když se to doporučuje - v některých operačních systémech je vhodné, když má jméno souboru příponu, která definuje typ obsahu), ale nicméně hodnota v tomto rozměru se považuje za typ souboru, ze kterého mj. vyplývá, v jakých ostatních rozměrech se soubory tohoto typu pohybují. MD-make si to přečte u pravidla, které soubory tohoto typu generuje jako svůj cíl. Takové pravidlo musí být alespoň jedno pro každý typ. Teoreticky jich může být i více, pokud chceme dělat něco jiného třeba pro jednotlivé jazyky. V tom případě všechna taková pravidla musí uvádět shodný seznam rozměrů cíle. Nemusí ale dohromady pokrývat všechny hodnoty všech těchto rozměrů. * Příslušné proměnné s hodnotami jednotlivých rozměrů musí být obyčejné proměnné, obsahující seznam slov oddělených mezerami. MD-make v nich nehledá odkazy na jiné proměnné nebo dokonce makra. Pokud v nich narazí na dolar, hodí výjimku a odmítne pokračovat. Tyto proměnné zůstanou viditelné i ve vygenerovaném makefilu. * Žádná hodnota v žádném rozměru nesmí být totožná s nějakou hodnotou v jiném rozměru. Jinými slovy, máme-li hodnotu, můžeme z ní poznat i rozměr. (Tím se mimo jiné předchází nejednoznačnostem ve jménech souborů, která neobsahují všechny rozměry.) * Multidimenzionální šablonovité pravidlo lze označit jako takové a říct pro něj: * Ve kterých rozměrech se pohybuje cílový soubor: direktiva ''.md.for''. (Ostatní rozměry se ve jménu souboru vůbec neobjeví.) * Jaké jsou podmínky na hodnoty v jednotlivých rozměrech. (Standardně pomocí direktivy ''.md.if'', ale nějak zařídit, aby se podmínka pro rozměr ''.STATES'' (resp. poslední rozměr v seznamu) mohla defaultně vyjádřit přímo v pravidle. * MD-make rozgeneruje multidimenzionální pravidlo na řadu obyčejných pravidel, ve kterých se vystřídají všechny kombinace hodnot ve všech zúčastněných rozměrech. Tato pravidla už nejsou šablonovitá, takže nehrozí, že gnu make potom narazí na cyklické závislosti nebo jiné problémy. * Uvnitř příkazů lze použít nové proměnné ''$(*1)'', resp. místo jedničky jiné číslo, pro n-tou závislost. MD-make si najde pravidlo, kterým tato závislost vzniká, zjistí si z něj, v jakých rozměrech se pohybuje, a podle toho zkonstruuje jméno příslušného souboru, které na dané místo vloží. Beze změny ponechá ''$<'' a ''$^'', které budou fungovat samy od sebe, avšak pozor na ''$*'', které v MD pravidlech (na rozdíl od obyčejných šablonovitých pravidel) nemá smysl. * MD pravidlo končí povinně prázdným řádkem (dokonce i na konci souboru). * Není-li uveden parametr ''.md.for'', pravidlo se rozgeneruje pro všechny známé rozměry kromě posledního (u nás ''STATES'', ale může se jmenovat i jinak). * Parametr ''.md.fix'' obsahuje hodnoty, které jsou v tomto pravidle pevné, tj. pravidlo se nerozgenerovává pro ostatní hodnoty téhož rozměru. Není zatím dovoleno uvést více hodnot ve stejném rozměru (i když by to teoreticky mohlo sloužit k vymezení částečného rozgenerování). * Jestliže ''.md.fix'' obsahuje rozměr, který je současně uveden v ''.md.for'', znamená to, že cílový typ souboru se pohybuje v tomto rozměru, má jeho hodnotu uvedenou v cestě, akorát toto konkrétní pravidlo generuje tento soubor pouze pro jednu hodnotu v dotyčném rozměru. * Jestliže ''.md.fix'' obsahuje rozměr, který není současně uveden v ''.md.for'', znamená to, že cílový typ souboru tento rozměr nezná a nemá ho uveden v cestě, avšak některý ze zdrojových souborů tento rozměr má a potřebuje vědět, kterou hodnotu máme na mysli. Které ze zdrojových souborů hodnotu ''.md.fix'' využijí, poznáme z pravidel, která tyto soubory generují jako cílové a vymezují jejich rozměry. * Jestliže některý zdrojový soubor vyžaduje rozměr, který cílový soubor neobsahuje, a tento rozměr není zafixován, pravidlo se rozgeneruje i pro všechny hodnoty tohoto rozměru. Bude pak existovat několik konkurenčních pravidel, která vytvářejí tentýž cílový soubor. * ''.md.del'' odstraní rozměry z ''.md.for'' (nejvíce se hodí, když ''.md.for'' není uvedeno a defaultně tedy obsahuje všechny rozměry) * ''.md.fxd'' je jako ''.md.fix'' a ''.md.del'' dohromady. Uvádějí se hodnoty, nikoli názvy rozměrů (tedy jako u ''.md.fix'' a na rozdíl od ''.md.del'') * Odkaz na hodnotu rozměru z příkazu (např. ''$(*LANGUAGES)'') se převede na aktuální hodnotu daného rozměru. Pokud mohou mít různé zdrojové soubory různé hodnoty téhož rozměru v rámci jednoho vygenerovaného pravidla, odkaz se převede na hodnotu, které v tomto rozměru nabývá cílový soubor, resp. která je proměnná. Odkazy tohoto druhu byly stejně zavedeny kvůli proměnným rozměrům. Odlišné hodnoty u konkrétních zdrojových souborů jsou fixní výjimky, tyto hodnoty známe předem a v případě potřeby je můžeme do příkazu zapsat přímo. .MDRULE .md.rul mst.conll < blind.conll mst .md.dep $(TOOLDIR)/runmst.pl .md.for: LANGUAGES DE PREPROCESSINGS .md.fix: test @echo Running MST for language $(*LANGUAGES): $(TOOLDIR)/runmst.pl -m $(*2) < $< > $@ * Je možné definovat vstupní soubory. Ty typicky leží úplně v jiné cestě, nebo se alespoň jmenují tak, aby se nepletly se soubory pojmenovanými pomocí hodnot rozměrů, a nehrozilo tudíž jejich smazání makem. Můžeme popsat jejich vlastnosti v jednotlivých rozměrech prostě tak, že vytvoříme obyčejné pravidlo, kde dotyčný vstupní soubor bude jako závislost, zatímco cíl bude soubor pojmenovaný příslušnými hodnotami rozměrů. Před pravidlo připíšeme ''.md.in:''. MD-make pak doplní příkaz pro zkopírování závislosti do cíle (''cp $< $@'') a navíc zkontroluje, že cílový soubor má hodnoty všech rozměrů, které soubor v daném stavu (hodnota posledního rozměru) má mít. * Vygenerovaný makefile by navíc mohl obsahovat pro každou hodnotu každého rozměru seznam souborů, v nichž je tato hodnota zafixovaná. Např. všechny cílové soubory v jazyce "hi". Kromě proměnné obsahující jména těchto souborů (HIFILES) by vygenerovaný makefile obsahoval cíl, který všechny tyto soubory vyrobí (hi), a cíl, který je smaže (clean_hi). * V průběhu generování vícerozměrných pravidel si pamatovat seznam všech vygenerovaných cílových souborů. Ke každému cílovému souboru vytvořit hash, jehož klíčem je hodnota libovolného rozměru a hodnota u daného klíče je nenulová, jestliže příslušná hodnota rozměru je v názvu souboru obsažena. Na konci makefilu lze použít pravidlo ''.MDALL'', které vytvoří ''.PHONY'' cíl závisející na všech souborech obsahujících určité hodnoty. Např. .MDALL: d hi conll se přepíše jako .PHONY: all_d_hi_conll all_d_hi_conll: Pozor! Podporu pro odesílání cílů na cluster, plánovanou níže, psát nemusím! Existuje totiž ''qmake'', který si poradí s normálním makefilem pro GNU make a sám rozesílá úlohy na cluster. Jediný podstatný rozdíl, na který je třeba dát pozor, je, že pravidlo nesmí obsahovat několik příkazů na samostatných řádcích. Pokud má obsahovat více než jeden příkaz, musejí být všechny na jednom řádku oddělené středníky a před případnými zalomeními řádku musí být backslash. * Podpora pro odesílání cílů na cluster: * Umět říct, který cíl chceme na clusteru, pokud to jde. * Detekce, že jsme na hlavě clusteru (HOSTNAME musí odpovídat nějaké proměnné). * Normální make bude puštěn paralelně (tuším -p 20 nebo tak nějak), aby dokázal řešit nezávislé další cíle, zatímco jeden cíl čeká na cluster. * K tomu je třeba novější verze mého ''qsub.csh'', která umí počkat, až odeslaný job skončí, a pokud skončí neúspěchem, umí ho navíc odeslat znova. (Musíme jí ale umět říct, jak se pozná neúspěch.)