close

文章導讀

煉丹總是效率低下該怎麼辦?本文作者總結了自己多年來的煉丹經驗,給大家提供了一些常見問題的解決方法和一套自建的工作流程。

來源丨https://zhuanlan.zhihu.com/p/482876481;作者:歐澤彬,西湖大學;

煉丹多年,輾轉在不同地方待過,發現還是有相當部分的小夥伴在手動敲命令開所有的實驗。高效一點的操作是寫一個 bash script 然後用 for loop 把實驗跑完,但似乎每次跑實驗效率還是比較低,一輪下來也相當累人。

總結下來有以下問題:

效率。在迭代的早期通常會用小模型加小數據。單個模型往往只需占用一張卡,在常見的 8 卡服務器中便留着其他卡在那乾等。
認知負載。訓一個模型涉及到非常冗長的 pipeline 以及眾多超參:數據處理,模型結構,模型參數,訓練參數,測試參數。這些可調的節點通常分布在代碼、數據或者命令行的參數裡面。這讓檢查、排錯和調整極其費勁。往往開完一組實驗一天的精力就見底了,更不用說隨時出現的錯誤會讓這組實驗的結果白白報廢。
可用性。怎麼在文件系統裡面區分這些不同的實驗?怎麼樣高效地區分並分析這些實驗的結果?怎麼樣把在一個 project 裡面開發的工具快速遷移到其他的 project?
魯棒性。如果機器突然宕機,哪些實驗需要重新跑一遍?

筆者深受這些痛點折磨,所以一直也在尋找解決方案。現在迭代的工作流感覺還算滿意,歡迎評論指教:

把所有的模型、流程改動都映射到命令行參數。模型結構變化用 if/else 或者 switch/case 引出來。這符合把 operation 變成 code 的主流趨勢,也能夠無縫對接到大多數主流代碼框架。
把不關心的默認參數放到一個「命令模板」裡面,然後將感興趣的參數變成變量,將感興趣的的取值組合寫到一個文件裡面。把 baseline 取值也寫到文件里,方便對比。
把文件當做任務池,用一組 worker (分配了 gpu 資源) 並發地去拉任務,跑任務。每個任務的中間結果寫到以超參組合命名的文件夾中,這比時間戳更可讀,也有足夠區分不同實驗,還可以查重防止重複跑實驗。用 tensorboard 跟蹤訓練進程。
寫一個評價標準和感興趣指標的 parser 對所有實驗的中間文件進行處理,再把結果拉到 jupyter notebook 或者 excel 表里做可視化和分析。

為了降低認知負擔,1 做了第一層簡化:把分散在各個地方的調節點全部抽象成命令行參數,這樣只需在開發的時候保證每個調節點都正常 work 就行了,查錯和決策就到了參數選擇的層面。2 則是做了第二層簡化:將無關的參數和實驗相關的參數區分開,這樣就避免了不小心改到別的參數而出錯 -- 而寫出這些無關的參數又迫使自己思考和過一遍所有可能的影響因子,查漏補缺。3 是實現層面的,任務池 + 並發的模式可以最大化硬件利用率,不需要惦記實驗有沒有跑完。處理完這幾點後,基本上工作流就變成了白天讀 paper、溝通、debug、分析結果,晚上回家前列好要跑的實驗跑起來,到家後看一下實驗是否正常運行,然後就可以倒頭睡覺等第二天出結果了。

基本原則講完之後也貼一個我的 python 實現。工具路徑在這裡,希望大家實驗跑的比誰都快:

https://github.com/simtony/runner

歡迎使用,star,二次開發,提 issue。非常相似的工具有微軟的 NNI(https://github.com/microsoft/nni),但是作為跑實驗的工具來說太重了,而且做了太多抽象,很難對某個實驗的結果做直觀的分析。另外這個回答(https://www.zhihu.com/question/384519338/answer/2152639948)提到的工具雖然實現了並發實驗的功能,但感覺不太夠用。如果有更好的方案可以評論區分享一下。

舉個栗子

假設現在我們開發了一個新的 normalization 層叫 「newnorm」,baseline 是 batchnorm。每一個實驗涉及 train、checkpoint average 和 test 三個流程。現在希望看不同的 normalization 以及不同的 momentum 參數對結果的影響,對應的配置文件如下:

---template:train:>pythontrain.pydata-bin/{data}--seed1--criterionlabel_smoothed_cross_entropy--archtransformer_iwslt_de_en--share-all-embeddings--optimizeradam--adam-betas'(0.9,0.98)'--clip-norm0.0--dropout0.3--lr-schedulerinverse_sqrt--warmup-updates8000--lr0.0015--min-lr1e-09--label-smoothing0.1--weight-decay0.0001--max-tokens4096--save-dir{_output}--tensorboard-logdir{_output}--no-save-optimizer-state--update-freq1--log-formatsimple--log-interval50--ddp-backendno_c10d--keep-last-epochs5--early-stop5--normalization{norm}[moment]avg:>pythonscripts/average_checkpoints.py--inputs{_output}--num-epoch-checkpoints5--output{_output}/averaged_model.pttest:>pythongenerate.pydata-bin/{data}--max-tokens4096--beam5--lenpen1.0--remove-bpe--path{_output}/averaged_model.pt--gen-subsettestdefault:data:iwslt14norm:batchmoment:0.1resource:[0,1,2,3]---norm:[new,batch]moment:[0.1,0.05]

第一個 yaml doc 作為實驗的 specification。template 下面指定了 train, checkpoint average 和 test 的模板命令,其中需要調的參數用{param}作為占位符。工具還定義了一些默認的參數,比如這個實驗對應的路徑{_output}。指定了要調的超參後,default 裡面指定了這些超參的 baseline 值,最後在 resource 里指定了 4 個 worker,每個 worker 對應一個 GPU。

從第二個 yaml doc 開始指定要格點搜的超參。默認會把所有超參組合跑一遍。這裡有 4 個任務。同步代碼和配置文件到服務器後,直接run並發地跑這4個任務:

$ runOrphan params: set()Tasks: 4, commands to run: 12START gpu: 0, train: 1/ 4, output/Norm_new-Moment_0.1START gpu: 1, train: 2/ 4, output/Norm_new-Moment_0.05START gpu: 2, train: 3/ 4, output/Norm_batch-Moment_0.1START gpu: 3, train: 4/ 4, output/Norm_power-Moment_0.05START gpu: 2, avg : 3/ 4, output/Norm_batch-Moment_0.1FAIL gpu: 2, avg : 3/ 4, output/Norm_batch-Moment_0.1...

每個輸出文件夾裡面會寫入相應的文件

$ ls output/Norm_batch-Moment_0.1checkpoint51.ptcheckpoint52.ptaveraged_model.ptlog.train.20220316.030151log.avg.20220316.030151log.test.20220316.030151paramstat

其中log.*是每個任務本來會打到命令行裡面的 log。param是每個任務對應的一些參數設定,方便 debug,stat則是任務狀態,分為success和fail。這可以用來幫助工具判斷是否需要重跑,也可以後期debug。跑實驗的過程可以開 tensorboard 跟蹤結果,一旦不對勁馬上 kill。

實驗跑完之後可以開一個 jupyter notebook 寫實驗結果分析的 parser。在這個例子裡面只需要從log.test裡面讀出 BLEU 就好了。寫完之後可以調用Examiner對所有結果做分析:

fromrunner.examineimportExaminer,latest_log#defineametricparserforeachdirectory(experiment)defadd_bleu(output_dir,experiment,caches):#Eachparserfollowsthesamesignature#Itcanread/writetoaglobalcachedict`caches`,#andread/writeeachexperiment:#collections.namedtuple("Experiment",["cache","metric","param"])latest_test_log=latest_log("test",output_dir)bleu=parse_bleu(latest_test_log)#auser-definedlogparserexperiment.metric["test_bleu"]=bleuexaminer=Examiner()#containerforparsedresults#registerparserforeachdirectory(experiment)examiner.add(add_bleu)#runallparsersfordirectoriesmatchedbyregexexaminer.exam(output="output",regex=".*batch.*")#printthetsvtablewithall(different)paramsandmetricsofeachexperimentexaminer.table()為什麼這樣寫

格點搜索可以適配大多數調參的場景。首先隨機暴力格點搜比較有效的調參方式,特別是當計算資源比較充足的時候。其次做對比實驗的時候也會用到參數的格點組合。最後如果不想格點搜,可以手動把想跑的超參組合各自寫到配置文件里。

每一個實驗都是由一系列順序執行的命令組成的,比如上述例子的 train - checkpoint average - test。所以相比簡單的命令,打包後的順序執行的命令是更好的任務池的單元。

超參配置模式先後有兩個版本。一開始是直接定義一個 config 類並對其操作。但是這樣會跟當前 project 深度耦合,換一個代碼庫就得改很多地方,還會出錯,並發部分也不好遷移到其他任務。最後將任務抽象成了一組命令,把修改超參轉化成修改任務命令,然後借用了 python 調用 bash 的接口進行並發跑任務。這個方案完美匹配各大主流框架。

並發部分前後迭代了三個版本。第一版的 multiprocessing 最簡單,但是對主進程 Ctrl + C 後經常出現 orphan process,還需要查 pid 手動去 kill。第二版的 thread 雖然沒有 orphan process 的問題,但是和 multiprocessing 一樣需要對全局共享的隊列和 io 加鎖,也很麻煩。最後收斂到了 asyncio 的 coroutine。後續加 cursor 的用戶界面也好寫一點。

一些不怎麼高級的進階功能

單個 worker 需要多 GPU 的話可以在 resource 裡面用引號框起來:resource: ["0,1", "2,3"]。

日常需要一組實驗在多個機器跑。不同機器卡數不同,需要跑的任務也不同。我先是用了 pycharm 的 deployment -> server group 的配置,讓每次Ctrl + S都會把本地代碼 push 到所有服務器上。在上述工具方面做了幾個改動:在命令行工具run中增加了-t和-r。其中-t可以指定跑 yaml 文件中對應_title參數的任務。-r指定 gpu index,這樣在不同機器通過命令行參數修改資源和 worker 數量。

經常會出現跑一個 train 和多個 test 的情況。為了避免每次 test 都得從頭跑一次 train,加了-c命令來選擇要跑的 command。同時在 yaml 文件裡面也加了_cmd字段方便按每組實驗配置。

一個參數打包很多超參的情況也非常常見。典型的如 Transformer 的 pre/post layernorm 需要同時改 encoder 和 decoder 的 normalization 方式。在切換數據集的時候也是如此,不同數據往往意味着一整套超參的改變。所以在 yaml 的第一個文檔中加了alias字段,用來將某一個參數的取值映射到一組參數的取值。

有的時候快速試一些改進的時候會懶得把它引到命令行參數裡面。這個時候為了將當前結果和已有結果區分開,可以在參數選擇中引入 template 裡面不用的參數。這些參數只會起到改輸出路徑名的作用。
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()