რამდენიმე კვირის განმავლობაში მივაღწიეთ 5-ჯერ შეყოვნების შემცირებას (და 50-ჯერ მეტ გამტარუნარიანობას). გვინდოდა გაგვეზიარებინა ყველა ის სირთულე და ეპიკური გამარჯვება, რაც გავიარეთ ასეთი სიჩქარის გაუმჯობესების მისაღწევად. ამ პროცესში მრავალი განსხვავებული ადამიანი იყო ჩართული მრავალ ეტაპზე, ამიტომ ყველაფერი აქ არ იქნება განხილული. გთხოვთ, მოგვიტევოთ, თუ ზოგიერთი შინაარსი შეიძლება მოძველებული ან არასწორი იყოს, რადგან ჩვენ ჯერ კიდევ ვსწავლობთ უზარმაზარი მოდელების ოპტიმიზაციას და მრავალი ახალი აპარატურული ფუნქცია და კონტენტი რეგულარულად ჩნდება. თუ თქვენი საყვარელი ოპტიმიზაციის მეთოდი არ არის განხილული ან არასწორად არის წარმოდგენილი, ბოდიშს გიხდით, გთხოვთ, გაგვიზიაროთ იგი, ჩვენ სიამოვნებით ვცდით ახალ რაღაცეებს და გამოვასწორებთ ჩვენს შეცდომებს. ცხადია, რომ თავდაპირველად დიდი მოდელის ხელმისაწვდომობის გარეშე, მისი ინფერენციის ოპტიმიზაციის რეალური მიზეზები არ იარსებებდა. ეს იყო წარმოუდგენელი ძალისხმევა, რომელსაც მრავალი სხვადასხვა ადამიანი ხელმძღვანელობდა. GPU-ს მაქსიმალური გამოყენებისთვის ტრენინგის დროს, განიხილეს რამდენიმე გადაწყვეტა და საბოლოოდ, საბოლოო მოდელის მოსამზადებლად აირჩიეს Megatron-Deepspeed. ეს ნიშნავდა, რომ კოდი, როგორც არის, აუცილებლად არ იყო თავსებადი `transformers` ბიბლიოთეკასთან. ორიგინალური ტრენინგის კოდის გამო, ჩვენ გადავწყვიტეთ გაგვეკეთებინა ის, რასაც რეგულარულად ვაკეთებთ: არსებული მოდელის `transformers`-ში პორტირება. მიზანი იყო ტრენინგის კოდიდან შესაბამისი ნაწილების ამოღება და მათი `transformers`-ში იმპლემენტაცია. ამ ძალისხმევას იუნესი ხელმძღვანელობდა. ეს არავითარ შემთხვევაში არ იყო მცირე ძალისხმევა, რადგან მას თითქმის ერთი თვე და 200 კომიტი დასჭირდა. რამდენიმე მნიშვნელოვანი რამ უნდა აღინიშნოს, რასაც მოგვიანებით დავუბრუნდებით: გვჭირდებოდა უფრო მცირე მოდელები: `bigscience/bigscience-small-testing` და `bigscience/bloom-560m`. ეს უკიდურესად მნიშვნელოვანია, რადგან ისინი უფრო მცირე ზომის არიან, ამიტომ მათთან მუშაობისას ყველაფერი უფრო სწრაფია. პირველ რიგში, უნდა მიატოვოთ ყველა იმედი იმისა, რომ ზუსტად იგივე ლოგიტები გექნებათ ბოლოს, ბაიტების დონეზე. PyTorch-ის ვერსიებს შეუძლიათ შეცვალონ ბირთვები და შეიტანონ დახვეწილი განსხვავებები, ხოლო სხვადასხვა აპარატურამ შეიძლება განსხვავებული შედეგები გამოიღოს სხვადასხვა არქიტექტურის გამო (და თქვენ ალბათ არ გსურთ მუდმივად A100 GPU-ზე მუშაობა ხარჯების მიზეზით). კარგი, მკაცრი ტესტების ნაკრების ქონა ნამდვილად მნიშვნელოვანია ყველა მოდელისთვის. საუკეთესო ტესტი, რომელიც ვიპოვეთ, იყო ფიქსირებული მოთხოვნების ნაკრები. თქვენ იცით მოთხოვნა, თქვენ იცით დასრულება, რომელიც უნდა იყოს დეტერმინისტული (greedy). თუ ორი გენერაცია იდენტურია, შეგიძლიათ უგულებელყოთ ლოგიტების მცირე განსხვავებები. ყოველთვის, როცა ხედავთ გადახრას, საჭიროა მისი გამოძიება. ეს შეიძლება ნიშნავდეს, რომ თქვენი კოდი არ აკეთებს იმას, რაც უნდა, ან რომ თქვენ რეალურად ხართ მოდელის დომენის მიღმა და შესაბამისად, მოდელი უფრო მგრძნობიარეა ხმაურის მიმართ. თუ გაქვთ რამდენიმე მოთხოვნა და საკმარისად გრძელი მოთხოვნები, ნაკლებად სავარაუდოა, რომ შემთხვევით გამოიწვევთ ამას ყველა მოთხოვნისთვის. რაც მეტი მოთხოვნა, მით უკეთესი, რაც უფრო გრძელი, მით უკეთესი. პირველი მოდელი (`small-testing`) არის bfloat16 ფორმატში, ისევე როგორც დიდი Bloom, ამიტომ ყველაფერი ძალიან მსგავსი უნდა ყოფილიყო, მაგრამ ის ბევრი არ იყო ნავარჯიშები ან უბრალოდ არ მუშაობს კარგად, ამიტომ მისი გამომავალი მონაცემები ძლიერ მერყეობს. ეს ნიშნავდა, რომ პრობლემები გვქონდა ამ გენერაციის ტესტებთან. მეორე მოდელი უფრო სტაბილურია, მაგრამ ის მომზადდა და შეინახეს float16-ში bfloat16-ის ნაცვლად. ეს ორ მოდელს შორის შეცდომის მეტ ადგილს ტოვებს. სამართლიანობისთვის, bfloat16 -> float16 კონვერტაცია, როგორც ჩანს, ნორმალური იყო ინფერენციის რეჟიმში (bfloat16 ძირითადად არსებობს დიდი გრადიენტების სამართავად, რომლებიც არ არსებობს ინფერენციისას). ამ ეტაპზე აღმოჩენილ იქნა და დაინერგა ერთი მნიშვნელოვანი კომპრომისი. რადგან Bloom მომზადდა განაწილებულ გარემოში, კოდის ნაწილი აკეთებდა ტენზორულ პარალელიზმს Linear layer-ზე, რაც იმას ნიშნავდა, რომ იგივე ოპერაციის ერთჯერადი ოპერაციის სახით გაშვება ერთ GPU-ზე სხვადასხვა შედეგს იძლეოდა. ამის დადგენას გარკვეული დრო დასჭირდა და ჩვენ ან უნდა გვევლო 100%-იანი თავსებადობის გზით და მოდელი იქნებოდა ბევრად ნელი, ან მივიღებდით მცირე განსხვავებას გენერაციაში, მაგრამ გაშვება იქნებოდა ბევრად სწრაფი და კოდი უფრო მარტივი. ჩვენ ავირჩიეთ კონფიგურირებადი დროშა. ახლა გვაქვს `transformers`-ის სამუშაო, სუფთა ვერსია, რათა დავიწყოთ მისი გაშვება. Bloom არის 352GB (176 მილიარდი პარამეტრი bf16-ში) მოდელი, მის განსათავსებლად გვჭირდება მინიმუმ ამდენი GPU RAM. მოკლედ განვიხილეთ CPU-ზე გადმოტვირთვა მცირე ზომის მანქანებზე, მაგრამ ინფერენციის სიჩქარე რიგითობით ნელი იყო, ამიტომ ეს ვარიანტი უარვყავით. შემდეგ გვინდოდა ძირითადად `pipeline`-ის გამოყენება. ეს არის ის, რასაც API მუდმივად იყენებს. თუმცა, `pipelines` არ არის განაწილებული სისტემებისთვის განკუთვნილი (ეს მათი მიზანი არ არის). ვარიანტების მოკლე განხილვის შემდეგ, საბოლოოდ გამოვიყენეთ `accelerate`-ის ახლად შექმნილი `device_map="auto"` მოდელის შარდინგის სამართავად. მოგვიწია რამდენიმე შეცდომის გამოსწორება და `transformers`-ის კოდის ოდნავ დაფიქსირება, რათა `accelerate`-ს სწორად ემუშავა. ის მუშაობს `transformers`-ის სხვადასხვა ფენის გაყოფით და მოდელის ნაწილის თითოეული GPU-სთვის გადაცემით. ასე რომ, GPU0 იწყებს მუშაობას, შემდეგ გადასცემს GPU1-ს და ასე შემდეგ. საბოლოოდ, პატარა HTTP სერვერით, ჩვენ შევძელით Bloom-ის (დიდი მოდელის) სერვისის დაწყება! მაგრამ ჩვენ ჯერ არც კი დაგვიწყია ოპტიმიზაციების განხილვა! სინამდვილეში, საკმაოდ ბევრი რამ გვაქვს, ეს მთელი პროცესი ქვიშის ციხესიმაგრეა.