გეცნობათ? ჩვენც იგივე გამოცდილება გვქონდა. ჩვენს nanoVLM პროექტზე დეტექტიური მუშაობის შემდეგ აღმოვაჩინეთ, რომ რეალური პრობლემა ჩვენი მოდელი ან აპარატურა კი არ იყო, არამედ მონაცემთა მილსადენი, რომელიც წარმოუდგენლად არაეფექტურად მუშაობდა. აი, რა აღმოვაჩინეთ: ამ პოსტში ჩვენ ვაშენებთ ეფექტურ მილსადენს ხუთ ეტაპად. თითოეულ ეტაპზე ვამატებთ ან ვაშორებთ წინა ნაბიჯიდან და ვაკეთებთ კომენტარებს იმაზე, თუ რა გაკეთდა სწორად და რა - არა. მონაცემთა მომზადების ამოცანების გასაადვილებლად, ჩვენ შევქმენით ცალკე რეპოზიტორიუმი, რომელიც მხოლოდ მონაცემთა მილსადენზეა ორიენტირებული. ვიმედოვნებთ, რომ ეს ბევრად უფრო ადვილი გასაგები იქნება, ვიდრე კოდის წაკითხვა nanoVLM რეპოზიტორიუმში ინტეგრირების შემდეგ. გარდა ამისა, ეს შეიძლება სასარგებლო იყოს სხვა მონაცემთა მილსადენების შესაქმნელადაც! რეპოზიტორიუმი: https://github.com/ariG23498/mmdp იმისათვის, რომ გაყვეთ, საკმარისია რეპოზიტორიუმის კლონირება. ის შეიცავს მონაცემთა მომზადების საბოლოო ამოცანებს, მაგრამ შექმნილია იმისთვის, რომ აჩვენოს თითოეული ნაბიჯი. რაიმეს ოპტიმიზაციამდე, უნდა გავიგოთ, რასთან გვაქვს საქმე. ჩვენი მულტიმოდალური მონაცემთა ნაკრები მოიცავს სურათებს, ტექსტურ მოთხოვნებს და პასუხებს. თქვენი სავარჯიშო მონაცემების გაცნობა გადამწყვეტია წარმატებისთვის. წინა სკრიპტი ყოველ ჯერზე აჩვენებს შემთხვევით ნიმუშს; შესაძლოა მოგინდეთ ამ ნაწილის ნოუთბუქში კოპირება და მისი რამდენჯერმე გაშვება, რათა მონაცემებზე წარმოდგენა შეგექმნათ. ჩვენი პირველი ვარჯიშის მცდელობისას გამოვიყენეთ აშკარა (და ძალიან ხშირი) მიდგომა: შედეგები მტკივნეული იყო. შეხედეთ ამ ვიზუალიზაციას: ხედავთ ამდენ ნაცრისფერს? ეს არის შევსება (padding). ეს ნიშნავს, რომ GPU აბსოლუტურად არაფერს ამუშავებს, მაშინ როცა თქვენ იხდით გამოთვლითი დროისთვის. ჩვენი ბეჩის დაახლოებით 60% ცარიელ ტოკენებზე იხარჯებოდა. ჩვენი შემდეგი ნაბიჯი მარტივი იყო. დავაწესეთ გლობალური მაქსიმალური სიგრძე და მივყევით მას. თუ ნიმუში ძალიან გრძელი იყო, უბრალოდ ამოვიღებდით. როგორც შეამჩნევდით, ბეჩს ახლა ერთი ნიმუში აკლია. ეს ფილტრაციის პროცესის გამოა. ამან გარკვეულწილად ხელი შეუწყო, მაგრამ ჩვენ მაინც ვავსებდით ყველაფერს ერთსა და იმავე ფიქსირებულ სიგრძემდე, მიუხედავად რეალური შინაარსისა. უკეთესი იყო ვიდრე ადრე, მაგრამ მაინც არაეფექტური. ახლა მზად ვართ, რომ ბეჩინგი მთლიანად გადავაფასოთ. შევსება (padding) ჩვენი მტერია და გვჭირდება სტრატეგია მისი მინიმიზაციისთვის, იმის მაქსიმალურად გაზრდისას, თუ რამდენი მონაცემის მოთავსება შეგვიძლია თითოეულ ბეჩში. აქ შემოდის ზურგჩანთის ამოცანა (knapsack problem), კომპიუტერული მეცნიერების კლასიკა, რომელიც იდეალურია ამისთვის. წარმოიდგინეთ, რომ ლაშქრობისთვის ზურგჩანთას ალაგებთ. მასში მხოლოდ გარკვეული წონის ჩადება შეგიძლიათ და გსურთ, რომ რაც შეიძლება მეტი სასარგებლო ნივთი ჩატენოთ. ჩვენს შემთხვევაში: ამ იდეის შესამოწმებლად, ვიწყებთ მარტივი მონაცემთა ნაკრებით: უბრალოდ რიცხვების სია 1-დან 25-მდე, რომელთაგან თითოეული წარმოადგენს მიმდევრობის სიგრძეს. ეს საშუალებას გვაძლევს ექსპერიმენტები ჩავატაროთ სურათებისა და ტექსტის სირთულეების გარეშე. PyTorch-ის მონაცემთა ნაკრებების უმეტესობა map-style-ია (მათზე წვდომა dataset[i]-ით ხდება). მაგრამ დინამიური ბეჩინგისთვის, ჩვენ გვჭირდება რაღაც უფრო მოქნილი. ამიტომ, ავაშენეთ iterable-style მონაცემთა ნაკრები torch.utils.data.IterableDataset-ის ქვე-კლასის შექმნით. ეს საშუალებას გვაძლევს, ბეჩები გენერირდეს მყისიერად და გავუმკლავდეთ ისეთ ხრიკებს, როგორიცაა მონაცემების გადანაწილება (sharding) მრავალ მუშაკზე: მიმდევრობების შეფუთვა შეიძლება ნელი იყოს, განსაკუთრებით თუ ვახარისხებთ ან ვურევთ. პროცესის დასაჩქარებლად, ვიყენებთ მწარმოებელ-მომხმარებლის პატერნს Python-ის რიგების (queues) გამოყენებით: მწარმოებლის თრეადი ალაგებს ბეჩებს და ათავსებს რიგში, ხოლო მთავარი თრეადი მათ საჭიროებისამებრ იღებს. ეს გადაფარვა უზრუნველყოფს მილსადენის შეუფერხებელ მუშაობას. პირველ რიგში, ვცდით მარტივ ხარბ შეფუთვის სტრატეგიას (greedy packing strategy): ეს თანმიმდევრულად გადის მონაცემებში, ამატებს ელემენტებს შეკვრაში, სანამ ის არ შეივსება, შემდეგ კი ახალს იწყებს. ის სწრაფია, მაგრამ არ არის სრულყოფილი. აი, როგორ გამოიყურება ბეჩები: შეამჩნიეთ, როგორ მეჩხერდება გვიანდელი ბეჩები? ჩვენ ვტოვებთ ხარვეზებს. მოდით, ვცადოთ უფრო ჭკვიანური მიდგომა: კონტეინერში ჩალაგება (bin-packing) (კერძოდ, First Fit Decreasing): ეს ახარისხებს მიმდევრობებს სიგრძის მიხედვით (ყველაზე გრძელი პირველი) და ცდილობს თითოეული მათგანის მორგებას პირველ პაკეტში, რომელშიც ადგილია. თუ არცერთი არ ჯდება, ის იწყებს ახალ პაკეტს. შედეგი? ეს ბეჩები ბევრად უფრო კომპაქტურია, ნაკლები რესურსის ხარჯვით. ეს ჰგავს ტეტრისის თამაშს თქვენს მონაცემებთან, ნაწილების მჭიდროდ ერთმანეთთან მორგებას. ახლა კი მთავარი: ზურგჩანთის შეფუთვის (knapsack packing) გამოყენება ჩვენს მულტიმოდალურ მონაცემთა ნაკრებზე. ჩვენ ვუბრუნდებით სურათებს, მოთხოვნებს და პასუხებს, და გვჭირდება მათი ეფექტურად შეფუთვა, ტოკენების ლიმიტებისა და სურათების ბიუჯეტის გათვალისწინებით. სურათების ბიუჯეტირება ისე ხდება, რომ თითო ნიმუშზე სურათები დაბალანსებული იყოს. გვსურს თავიდან ავიცილოთ შემთხვევა, როდესაც ერთ GPU-ს გაცილებით მეტი სურათის დამუშავება სჭირდება, ვიდრე მეორეს. ჩვენი ახალი ConstantLengthDataset კლასი ასრულებს ყველაზე რთულ სამუშაოს. აი, როგორ მუშაობს ის, მე-4 ეტაპთან შედარებით: ConstantLengthDataset ყველაფერს აკეთებს: აი, შედეგი: შეხედეთ! ნაცრისფერი (შევსება) მინიმალურია, ხოლო ბეჩები მკვრივია სასარგებლო მონაცემებით. ეს ჰგავს ჩემოდნის ისე კარგად ჩალაგებას, რომ მისი ელვაშესაკრავის შეკვრა მაინც შეგიძლიათ მასზე დაჯდომის გარეშე. სურათი შეიძლება თავიდან ინტუიციური არ მოგეჩვენოთ, მაგრამ მოდით შევხედოთ მას გვერდიგვერდ შეზღუდული შევსებით. აქ შეამჩნევთ, რომ ზურგჩანთაში ნიმუშები უფრო თანაბრად არის განაწილებული. ჩვენ ასევე არ ვაწყდებით პრობლემას, როდესაც ბეჩში ფილტრაციის გამო ნაკლები ნიმუშია. ის, რაც დაიწყო, როგორც მარტივი კითხვა „რატომ არის ვარჯიში ასეთი ნელი?“, მიგვიყვანა მულტიმოდალური მონაცემების დამუშავების სრულიად გადახედვამდე. მონაცემთა მილსადენისთვის დაბალანსებული ზურგჩანთის სტრატეგია მოდის Eagle 2: Building Po