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í 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.
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é.
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.
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.
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é.
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.
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.
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“.
.MDIMS: LANGUAGES/ DE TRAINTEST -PREPROCESSINGS .STATES
.md.for
. (Ostatní rozměry se ve jménu souboru vůbec neobjeví.).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.$(*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.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)..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í)..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..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..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
)$(*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) < $< > $@
.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..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: <seznam všech souborů obsahujících hodnoty "d", "hi" a "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.
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.)