python - Kompilering Executable med dask eller joblib multiprocessing med cython resulterer i fejl

Indlæg af Hanne Mølgaard Plasc

Problem



Jeg konverterer nogle serielt behandlede python-job til multiprocessing med dask eller joblib. Desværre skal jeg arbejde på windows.

Når du kører indefra IPython eller fra kommandolinjen, der påberåber py-filen med python, går det fint.

Når du kompilerer en eksekverbar med cython, løber den ikke længere fint: Trin for trin bliver flere og flere processer (ubegrænset og større end antallet af ønskede processer) startet og blokerer mit system.

Det føles på en eller anden måde som Multiprocessing Bomb - men selvfølgelig brugte jeg if \_\_name\_\_=="\_\_main\_\_:" til at have kontrolblokken - godkendt ved fint at køre fra python call på kommandolinjen.

Mit cythonopkald er cython --embed --verbose --annotate THECODE.PY og jeg 'compilerer med gcc -time -municode -DMS\_WIN64 -mthreads -Wall -O -I"PATH\_TO\_include" -L"PATH\_TO\_libs" THECODE.c -lpython36 -o THECODE, hvilket resulterer i en Windows-eksekverbar THECODE.exe.

Med anden (single processing) kode, der kører fint.

Problemet synes at være det samme for dask og joblib (hvad kan betyde, at dask fungerer som eller er baseret på joblib).

Eventuelle forslag?


For de interesserede i en mcve: Bare at tage den første kode fra Multiprocessing Bomb og kompilere den med mine cython kommandoer ovenfor vil resultere i en eksekverbar blæser dit system. (Jeg har lige prøvet :-))


Jeg har lige fundet noget interessant ved at tilføje en linje til kodeprøven for at vise \_\_name\_\_:


import multiprocessing

def worker():
    """worker function"""
    print('Worker')
    return

print("-->" + \_\_name\_\_ + "<--")
if \_\_name\_\_ == '\_\_main\_\_':
    jobs = []
    for i in range(5):
        p = multiprocessing.Process(target=worker)
        jobs.append(p)
        p.start()


Når du kører det stykke kode med python, vises det


\_\_main\_\_
\_\_mp\_main\_\_
\_\_mp\_main\_\_
\_\_mp\_main\_\_
\_\_mp\_main\_\_
\_\_mp\_main\_\_


(anden output undertrykt). Forklarer, at hvis afgørelsen virker.
Når du kører den eksekverbare efter cython og kompilering er viser


\_\_main\_\_
\_\_main\_\_
\_\_main\_\_
\_\_main\_\_
\_\_main\_\_
\_\_main\_\_


og mere og mere. Således er arbejdstagerne til modulet ikke længere masqueraded som en import, og hver arbejdstager forsøger derfor at starte fem nye på en rekursiv måde.

Bedste reference


Først var jeg overrasket over at se, at din cythonversion fungerede på en eller anden måde, men det er kun et udseende at arbejde. Men med noget hacking ser det ud til at være muligt at få det til at fungere.


Jeg er på linux, så jeg bruger mp.set\_start\_method('spawn') til at simulere opførelsen af ​​windows.


Hvad sker der i spawn -tilstanden? Lad os tilføje nogle sleep s, så vi kan undersøge processerne:


#bomb.py
import multiprocessing as mp
import sys
import time

def worker():
    time.sleep(50)
    print('Worker')
    return

if \_\_name\_\_ == '\_\_main\_\_':
        print("Starting...")
        time.sleep(20)
        mp.set\_start\_method('spawn') ## use spawn!
        jobs = []
        for i in range(5):
            p = mp.Process(target=worker)
            jobs.append(p)
            p.start()


Ved at bruge pgrep python kan vi se, at der først er kun én python-proces, derefter 7 (!) Forskellige pid s. Vi kan se kommandolinjeparametrene via cat /proc/<pid>/cmdline. 5 af de nye processer har kommandolinje


-c "from multiprocessing.spawn import spawn\_main; spawn\_main(tracker\_fd=5, pipe\_handle=11)" --multiprocessing-fork


og en:


-c "from multiprocessing.semaphore\_tracker import main;main(4)"


Det betyder, at forældelsesprocessen starter 6 nye pythoninterpreter-tilfælde, og hver nybegynder tolk udfører en kode sendt fra forælder via kommandolinjevalg, informationen deles via rør. En af disse 6 python-forekomster er en tracker, der observerer hele sagen.


Ok, hvad sker der hvis cythonized + embeded? Det samme som med den normale python, den eneste forskel er, at bomb - eksekverbar startes i stedet for python. Men forskelligt som python-tolken udfører det ikke/er ikke opmærksom på kommandolinjeparametrene, så funktionen main kører igen og igen.


Der er en nem løsning: lad bomb - exe starte python tolken


 ...
 if \_\_name\_\_ == '\_\_main\_\_':
    mp.set\_executable(<PATH TO PYTHON>)
 ....


Nu er bomb ikke længere en multiprocessing bombe!


Målet er dog sandsynligvis ikke at have en python-tolk rundt, så vi skal gøre vores program opmærksom på mulige kommandolinjer:


import re
......
if \_\_name\_\_ == '\_\_main\_\_':
    if len(sys.argv)==3:  # should start in semaphore\_tracker mode
        nr=list(map(int, re.findall(r'd+',sys.argv[2])))          
        sys.argv[1]='--multiprocessing-fork'   # this canary is needed for multiprocessing module to work   
        from multiprocessing.semaphore\_tracker import main;main(nr[0])

    elif len(sys.argv)>3: # should start in slave mode
        fd, pipe=map(int, re.findall(r'd+',sys.argv[2]))
        print("I'm a slave!, fd=\%d, pipe=\%d"\%(fd,pipe)) 
        sys.argv[1]='--multiprocessing-fork'   # this canary is needed for multiprocessing module to work  
        from multiprocessing.spawn import spawn\_main; 
        spawn\_main(tracker\_fd=fd, pipe\_handle=pipe)

    else: #main mode
        print("Starting...")
        mp.set\_start\_method('spawn')
        jobs = []
        for i in range(5):
            p = mp.Process(target=worker)
            jobs.append(p)
            p.start()


Nu har vores bombe ikke brug for en frittstående python-tolk og stopper efter arbejderne er færdige. Bemærk venligst følgende:



  1. Den måde, hvorpå det afgøres, i hvilken tilstand bomb skal startes, er ikke særlig fejlfrit, men jeg håber du får kernen

  2. --multiprocessing-fork er bare en kanariefugle, det gør ikke noget, det skal kun være der, se her.



Jeg vil gerne ende med en ansvarsfraskrivelse: Jeg har ikke meget erfaring med multiprocessing-modul og ingen på windows, så jeg er ikke sikker på, at denne løsning skal anbefales. Men i det mindste er det sjovt:) [47]





NB: Den ændrede kode kan også bruges med python, fordi efter "from multiprocessing.spawn import spawn\_main; spawn\_main(tracker\_fd=5, pipe\_handle=11)" --multiprocessing-fork python ændres sys.argv, så koden ser ikke længere den oprindelige kommandolinje, og len(sys.argv) er 1 .

Andre referencer 1


Inspireret af svaret (eller de givne ideer der) fra ead, fandt jeg en meget enkel løsning - eller lader bedre kalde det løsning.

For mig ændrer jeg blot if-klausulen til


if \_\_name\_\_ == '\_\_main\_\_':
    if len(sys.argv) == 1:
        main()
    else:
        sys.argv[1] = sys.argv[3]
        exec(sys.argv[2])


gjorde det.

Grunden til, at det virker, er (i mit tilfælde):
Når du ringer den oprindelige .py-fil, er arbejdstagerens \_\_name\_\_ indstillet til \_\_mp\_main\_\_ (men alle processer er blot den almindelige .py-fil).

Når man kører den (cython) kompilerede version, er arbejdstagerens name ikke brugbar, men arbejderne bliver kaldt forskellige, og derfor kan vi identificere dem ved mere det ene argument i argv. I min sag er medarbejderens argv læst


['MYPROGRAMM.exe',
 '-c',
 'from multiprocessing.spawn import spawn\_main;
       spawn\_main(parent\_pid=9316, pipe\_handle =392)',
 '--multiprocessing-fork']


Således i argv[2] er koden for aktivering af arbejderne fundet og udføres med de øverste kommandoer.

Selvfølgelig, hvis du har brug for argumenter for din kompilerede fil, har du brug for en større indsats, måske parsing for parent\_pid i opkaldet. Men i mit tilfælde ville det simpelthen være overdrevet.

Andre referencer 2


Jeg tror på baggrund af detaljerne fra den indsendte fejlrapport, kan jeg tilbyde den måske mest elegante løsning herinde [49]


if \_\_name\_\_ == '\_\_main\_\_':
    if sys.argv[0][-4:] == '.exe':
        setattr(sys, 'frozen', True)
    multiprocessing.freeze\_support()
    YOURMAINROUTINE()


freeze\_support() - Opkald er påkrævet på Windows - Se dokumentation for Python Multiprocessing.

Hvis du kun kører inden for python med den linje, er det allerede fint.

Men på en eller anden måde er cython naturligvis ikke opmærksom på nogle af disse ting (docs siger, at det er testet med py2exe, PyInstaller og cx\_Freeze). Det kan lindres af setattr - kaldet, som kun kan bruges ved kompilering, således beslutningen ved filtildeling. [50]