Ինչու՞ վճարել հզոր պրոցեսորի համար, եթե չես կարող օգտագործել այն ամբողջը:

8 միջուկով Intel i9–9900K-ը տատանվում է $450-ից $500-ի սահմաններում

Դա մեծ գումար է CPU-ի վրա ծախսելու համար:
Եվ եթե չես կարող այն օգտագործել իր առավելագույն չափով, ինչու՞ նույնիսկ ունենալ այն:

Բազմամշակումը թույլ է տալիս մեզ օգտագործել մեր պրոցեսորները իրենց առավելագույն չափով: Ծրագրերը տող առ տող գործարկելու փոխարեն մենք կարող ենք միաժամանակ գործարկել կոդի մի քանի հատված կամ կոդի նույն հատվածները մի քանի անգամ զուգահեռաբար: Եվ երբ մենք դա անում ենք, մենք կարող ենք այն բաժանել մեր պրոցեսորի մի քանի միջուկների միջև, ինչը նշանակում է, որ մենք կարող ենք շատ ավելի արագ հաշվարկել հաշվարկները:

Եվ, ի ուրախություն մեզ, Python-ն ունի ներկառուցված բազմամշակման գրադարան:

Գրադարանի հիմնական առանձնահատկությունը Process դասն է: Երբ մենք ներկայացնում ենք Process-ը, մենք նրան փոխանցում ենք երկու արգումենտ: target, ֆունկցիան, որը մենք ցանկանում ենք, որ այն հաշվարկի, և args, արգումենտները, որոնք ցանկանում ենք փոխանցել այդ թիրախային ֆունկցիային:

import multiprocessing
process = multiprocessing.Process(target=func, args=(x, y, z))

Դասը օրինականացնելուց հետո այն կարող ենք սկսել .start() մեթոդով:

process.start()

Unix-ի վրա հիմնված օպերացիոն համակարգերում, օրինակ՝ Linux, macOS և այլն, երբ գործընթացն ավարտվում է, բայց չի միացել, այն դառնում է զոմբի գործընթաց: Մենք կարող ենք դա լուծել process.join()-ով:

Այս հոդվածում մենք կքննարկենք, թե ինչպես կարելի է օգտագործել Python-ի բազմամշակման գրադարանը, որպեսզի բարձր լուծաչափով պատկերները բեռնավորվեն անփույթ զանգվածներում, և բավական երկար ժամանակահատվածում՝ խնայել հաշվարկների ժամերը:

Բայց նախքան իրական աշխարհի օրինակում բազմապրոցեսինգի իրականացմանը անցնելը, եկեք մի փոքրիկ խաղալիք սցենար պատրաստենք՝ ցույց տալու, թե ինչպես է այն աշխատում:

Մենք կարող ենք ստեղծել որոշ պատահական տվյալներ՝ մշակելու համար.

import numpy as np
fake_data = np.random.random((100, 1000000))

Այնուհետև մենք կարող ենք գրել մի գործառույթ, որը պատահական հաշվարկներ է կատարում տվյալների վրա.

if հայտարարությունը, որը ստուգում է, թե արդյոք data_pointNone է, տեխնիկապես անիմաստ է, քանի որ np.random.random-ը երբեք չի վերադարձնի None, բայց, անկախ նրանից, դա մեզ համար լրացուցիչ հաշվարկ է բազմամշակման միջոցով արագացնելու համար:

Մենք կարող ենք գնահատել, թե որքան ժամանակ է պահանջվում այս գործառույթը հաշվարկելու համար հետևյալով.

Իմ Intel i7–8700K 3,70 ԳՀց հաճախականությամբ պրոցեսորի վրա, այս աշխատանքը տևում է մոտավորապես 30 վայրկյան:

Դա շատ արագ չէ: Եվ կարելի է միայն պատկերացնել, թե ինչ տեսք կունենար, եթե տվյալներն ավելի մեծ լինեին, կամ հաշվարկներն ավելի թանկ պահանջեին:

Այսպիսով, հարց է առաջանում, թե ինչու է դա այդքան երկար տեւում հաշվարկելու համար:

Եկեք նայենք CPU-ի օգտագործման մատյանին, մինչ մենք աշխատում ենք սա.

Զարմանալի չէ, որ ֆունկցիան այդքան երկար է տևում, մենք ունենք մեր հաշվողական հզորության միայն մի մասը, որը հատկացված է այն գործարկելուն:

Եկեք դա շտկենք:

Նախքան սկսելը, մենք պետք է ներմուծենք ևս մեկ մոդուլ: Tenzing-ով SharedArray-ը Numpy զանգվածներ ստեղծելու մոդուլ է, որոնց հասանելի են համակարգչի տարբեր պրոցեսները:

Սովորական ndarray-ի օգտագործումը չի աշխատի, քանի որ յուրաքանչյուր գործընթաց ունի առանձին հիշողություն և չի կարող փոխել գլոբալ զանգվածը:

Մենք կարող ենք տեղադրել SharedArray հետևյալով.

pip install SharedArray

և այնուհետև ներմուծել այն հետևյալով.

import SharedArray

SharedArray-ն ունի մի քանի հիմնական գործառույթ.

  • SharedArray.create(name, shape, dtype=float)-ը ստեղծում է ընդհանուր հիշողության զանգված
  • SharedArray.attach(name) փոփոխականին կցում է նախկինում ստեղծված ընդհանուր հիշողության զանգվածը
  • SharedArray.delete(name)-ը ջնջում է ընդհանուր հիշողության զանգվածը, սակայն գոյություն ունեցող հավելվածները մնում են վավեր

Կան բազմաթիվ այլ օգտակար հատկություններ SharedArray-ն առաջարկում է, և ես խորհուրդ կտայի կարդալ PyPI էջը փաստաթղթերի համար:

Multiprocessing ֆունկցիայի իրականացում

Բազմամշակման ֆունկցիայի ներսում մենք կարող ենք ստեղծել ընդհանուր հիշողության զանգված.

Այժմ մենք պետք է սահմանենք multiprocess_data()-ի ներսում երեխա-գործառույթ, որը կհաշվարկի տվյալների առանձին տող:
Սա այնպես է, որ մենք կարողանանք փոխանցել multiprocessing.Process target ֆունկցիան, երբ մենք ստեղծենք մեր գործընթացները ավելի ուշ:

Այժմ, կեղծ տվյալների յուրաքանչյուր տողի համար մենք կարող ենք ստեղծել նոր Process և սկսել այն.

Եվ վերջապես, բոլոր գործընթացները սկսելուց հետո մենք կարող ենք .join() դրանք և վերադարձնել տվյալները.

Որպես ամփոփում, ահա ամբողջական գործառույթը, որն իրականացվում է բազմամշակումով.

Հիմա ժամանակն է գնահատել մեր բարելավումը:

Վազում…

…տպվում է մոտավորապես 6 վայրկյան:

Դա բավականին բարելավում է:

Եվ եթե ֆունկցիան գործարկելիս նայենք պրոցեսորի օգտագործման մատյանին…

Դա շատ ավելի լավ տեսք ունի:

Կարելի է հարցնել, եթե իմ պրոցեսորն ասում է, որ այն ունի 12 միջուկ, ինչո՞ւ է գործընթացը արագանում միայն 5-6 անգամ:
Շատ ժամանակակից պրոցեսորի ճարտարապետություններ օգտագործում են hyper-threading, ինչը նշանակում է, որ չնայած իմ ՕՀ-ին թվում է, որ ես ունեն 12 միջուկ; իրականում ես դրա միայն կեսն ունեմ, իսկ մյուս կեսը սիմուլյացված է:

Կարող եք նաև ջնջել ընդհանուր հիշողության ֆայլը հաշվարկների ավարտից հետո, որպեսզի սխալներ չառաջանան, եթե պատրաստվում եք վերագործարկել ձեր ծրագիրը:
Դուք կարող եք դա անել հետևյալով.

SharedArray.delete('data')

Multiprocessing իրական աշխարհում

Ենթադրենք, մենք Kaggle-ի մրցույթում ենք, որտեղ մենք պետք է բեռնենք բազմաթիվ պատկերներ: Հասկանալով ամպերը արբանյակային պատկերներից, ասենք. Միգուցե մենք ցանկանում ենք ստեղծել տվյալների գեներատոր ֆունկցիա, որը դուրս է հանում 300 պատկերների փաթեթ՝ ndarrray ձևով:

Ենթադրելով, որ մենք ունենք պատկերի բոլոր տվյալները «train_images» անունով թղթապանակում, մենք կգրենք մի ֆունկցիա…

…տվյալները բեռնելու համար:

  • os.listdir-ը վերադարձնում է գրացուցակի ամեն ինչի ցանկը, որը մեր դեպքում բոլոր պատկերների ֆայլերի անուններն են
  • cv2.imread-ը կարդում է պատկերը և այն ավտոմատ կերպով վերածում Numpy զանգվածի: Դուք կարող եք կարդալ ավելին OpenCV-ն անիվների վրա (cv2) տեղադրելու և օգտագործելու մասին այստեղ:

Իհարկե, այս գործառույթը չի վերադարձնում պատկերների պիտակները, և այն ամեն անգամ տալիս է նույն 300 պատկերը, ուստի այն ամբողջովին գործնական չէ, բայց այդ գործառույթը հեշտ է ավելացնել:

Այնուհետև մենք կարող ենք չափել այս ֆունկցիայի հաշվարկման ժամանակը, օգտագործելով նույն մեթոդները, որոնք մենք ունեինք ավելի վաղ, որն ինձ համար մոտավորապես 12–13 վայրկյան է:

Դա լավ չէ: Եթե ​​մենք պատրաստվում ենք դա անել գեներատորի ներսում՝ կանխատեսման մոդելի անցնելու համար, մեր ժամանակի մեծ մասը կծախսվի պատկերները բեռնելու վրա: Մենք չենք կարող բոլոր պատկերները միանգամից բեռնված պահել, քանի որ Numpy զանգվածներում միայն ~300 1400x2100 պատկեր բեռնելը պահանջում է 20–25 ԳԲ RAM:

Օգտագործելով այն հմտությունները, որոնք մենք սովորել ենք մեր խաղալիքի օրինակը բազմամշակելիս, մենք կարող ենք շատ արագացնել այս գործառույթը:

Նախ, մենք կարող ենք ստեղծել SharedArray մեր տվյալների համար.

Հաջորդը, մենք մի փոքր այլ բան ենք անում: Քանի որ մենք չունենք պրոցեսորի անսահման միջուկներ, ավելի շատ գործընթացներ ստեղծելը, քան մեր միջուկները, ի վերջո կդանդաղեցնի գործառույթը: Սա լուծելու համար մենք կարող ենք ստեղծել workers, որոնցից յուրաքանչյուրը կունենա որոշակի քանակությամբ պատկերներ բեռնելու համար՝ worker_amount:

Դուք կարող եք փոխել աշխատողների թիվը՝ ձեր պրոցեսորի բնութագրերին համապատասխանելու համար:

Դրանից հետո մենք կարող ենք ստեղծել թիրախային ֆունկցիա, որը յուրաքանչյուր աշխատող կհաշվի. ֆունկցիան կունենա մեկնարկային ինդեքս՝ i, իսկ բեռնվող պատկերների քանակը՝ n:

Այնուհետև մենք կարող ենք նոր գործընթաց սկսել յուրաքանչյուր աշխատողի համար և նրան նշանակել worker_amount պատկերներ՝ բեռնելու համար:

worker_amount*worker_num-ը մեզ տալիս է այն ինդեքսը, որով պետք է սկսենք բեռնել պատկերների հաջորդ հավաքածուն:

Եվ վերջապես, մենք կարող ենք .join() յուրաքանչյուր գործընթաց և վերադարձնել մեր ստեղծած տվյալները:

Որպես արագ ամփոփում, ահա ամբողջական գործառույթը, որը մենք պարզապես գրել ենք.

Այժմ օգտագործելով նույն մեթոդը, որը մենք օգտագործել ենք նախկինում, մենք կարող ենք ժամանակավորել մեր գործառույթը, որն ինձ համար տևում է 2 վայրկյանից մի փոքր պակաս:

Եթե ​​մենք դա անեինք հարյուրավոր կամ նույնիսկ հազարավոր անգամներ, ապա 11 վայրկյան խնայողությունը յուրաքանչյուր բեռնման նստաշրջանի համար հավասարազոր է 18 րոպե և 3 ժամ խնայված ժամանակի100 անգամ: եւ համապատասխանաբար 1000 անգամ։

Եզրակացություն

Վերոնշյալ օրինակը արտացոլում է ոչ միայն այն, թե որքան շահավետ կարող է լինել բազմամշակումը, այլև այն, թե որքան կարևոր է մեզ համար օպտիմալացնել այն հաշվարկները, որոնք մենք առավել հաճախ ենք անում:

Անարդյունավետ օգտագործվող մեկ for օղակը լրացուցիչ աշխատանքի վրա, բավական մեծ մասշտաբով, ընկերությանը կարժենա հարյուրավոր ժամեր և հազարավոր դոլարներ:

Նույնիսկ բազմամշակման բոլոր առավելություններով, մենք դեռ կարող ենք ավելի արագ գնալ: Ջորջ Սեյֆի այս փայլուն հոդվածը բացատրում է, թե ինչպես արագացնել տվյալների գիտությունը՝ օգտագործելով ձեր GPU-ն: Ձեր GPU-ն հատուկ նախագծված է զուգահեռ հաշվարկների համար, և երբ տրվում է բավականաչափ մեծ տվյալների բազա, էքսպոնենցիալ ավելի արագ է, քան ձեր պրոցեսորը:

Այնուամենայնիվ, GPU-ները սովորաբար ավելի դժվար են աշխատել և դուրս են այս հոդվածի շրջանակներից:

Ես նաև խորհուրդ կտայի ստուգել Manu NALEPAPandarallel գրադարանը, որը թույլ է տալիս բազմամշակում կատարել Pandas-ի տվյալների շրջանակների վրա:
Ցավոք, այս գրադարանը հասանելի է միայն Unix-ի վրա հիմնված օպերացիոն համակարգերի վրա՝ շնորհիվ իր հետին մասի:

Եվ ինչպես միշտ, այս հոդվածի կոդը հասանելի է այստեղ, իմ GitHub-ում:

Հետագա ընթերցում

Փաստաթղթեր և կայքեր.

Լրացուցիչ GPU արագացված Python հոդվածներ.