os.sched_getaffinity (0) vs os.cpu_count ()

Oct 04 2020

Jadi, saya tahu perbedaan antara kedua metode dalam judul, tetapi bukan implikasi praktisnya.

Dari apa yang saya pahami: Jika Anda menggunakan lebih banyak NUM_WORKERS daripada inti yang sebenarnya tersedia, Anda menghadapi penurunan kinerja yang besar karena OS Anda terus-menerus beralih bolak-balik mencoba untuk menjaga semuanya tetap paralel. Tidak tahu seberapa benar ini, tapi saya membacanya di sini tentang SO dari seseorang yang lebih pintar dari saya.

Dan di dokumen untuk os.cpu_count()itu tertulis:

Kembalikan jumlah CPU dalam sistem. Mengembalikan Tidak Ada jika tidak ditentukan. Jumlah ini tidak sama dengan jumlah CPU yang dapat digunakan proses saat ini. Jumlah CPU yang dapat digunakan dapat diperoleh dengan len (os.sched_getaffinity (0))

Jadi, saya mencoba mencari tahu apa yang dimaksud dengan "sistem" jika ada lebih banyak CPU yang dapat digunakan oleh suatu proses daripada yang ada di "sistem".

Saya hanya ingin mengimplementasikan multiprocessing.poolfungsionalitas dengan aman dan efisien . Jadi, inilah pertanyaan saya yang dirangkum:

Apa implikasi praktis dari:

NUM_WORKERS = os.cpu_count() - 1
# vs.
NUM_WORKERS = len(os.sched_getaffinity(0)) - 1

Ini -1karena saya telah menemukan bahwa sistem saya jauh lebih lamban jika saya mencoba bekerja saat data sedang diproses.

Jawaban

2 Booboo Oct 04 2020 at 20:18

Jika Anda memiliki tugas yang murni 100% terikat CPU, yaitu tidak melakukan apa pun selain kalkulasi, maka jelas tidak ada yang akan / dapat diperoleh dengan memiliki ukuran kumpulan proses yang lebih besar dari jumlah CPU yang tersedia di komputer Anda. Tetapi bagaimana jika ada campuran I / O yang dilemparkan di mana proses akan melepaskan CPU menunggu I / O selesai (atau, misalnya, URL dikembalikan dari situs web, yang membutuhkan waktu yang relatif lama ) ? Bagi saya, tidak jelas bahwa Anda tidak dapat mencapai dalam skenario ini peningkatan throughput dengan ukuran kumpulan proses yang melebihi os.cpu_count().

Memperbarui

Berikut adalah kode untuk menunjukkan intinya. Kode ini, yang mungkin paling baik disajikan dengan menggunakan threading, menggunakan proses. Saya memiliki 8 core di desktop saya. Program ini hanya mengambil 54 URL secara bersamaan (atau secara paralel dalam kasus ini). Program ini memberikan argumen, ukuran kolam yang akan digunakan. Sayangnya, ada overhead awal hanya untuk membuat proses tambahan sehingga penghematan mulai berkurang jika Anda membuat terlalu banyak proses. Tetapi jika tugas tersebut berjalan lama dan memiliki banyak I / O, maka overhead pembuatan proses akan sepadan pada akhirnya:

from concurrent.futures import ProcessPoolExecutor, as_completed
import requests
from timing import time_it

def get_url(url):
    resp = requests.get(url, headers={'user-agent': 'my-app/0.0.1'})
    return resp.text


@time_it
def main(poolsize):
    urls = [
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
        'https://ibm.com',
        'https://microsoft.com',
        'https://google.com',
    ]
    with ProcessPoolExecutor(poolsize) as executor:
        futures = {executor.submit(get_url, url): url for url in urls}
        for future in as_completed(futures):
            text = future.result()
            url = futures[future]
            print(url, text[0:80])
            print('-' * 100)

if __name__ == '__main__':
    import sys
    main(int(sys.argv[1]))

8 proses: (jumlah inti yang saya miliki):

func: main args: [(8,), {}] took: 2.316840410232544 sec.

16 proses:

func: main args: [(16,), {}] took: 1.7964842319488525 sec.

24 proses:

func: main args: [(24,), {}] took: 2.2560818195343018 sec.
2 Darkonaut Oct 04 2020 at 19:36

Kedua fungsi ini sangat berbeda dan NUM_WORKERS = os.sched_getaffinity(0) - 1akan langsung gagal TypeErrorkarena Anda mencoba mengurangi integer dari satu set. While os.cpu_count()memberi tahu Anda berapa banyak inti yang dimiliki sistem, os.sched_getaffinity(pid)memberi tahu Anda di inti mana utas / proses tertentu diizinkan untuk dijalankan.


os.cpu_count()

os.cpu_count()menunjukkan jumlah inti yang tersedia yang dikenal dengan OS ( inti virtual ). Kemungkinan besar Anda memiliki setengah dari jumlah inti fisik ini . Jika masuk akal untuk menggunakan lebih banyak proses daripada yang Anda miliki inti fisik, atau bahkan lebih dari inti virtual, sangat bergantung pada apa yang Anda lakukan. Semakin ketat loop komputasi (sedikit perbedaan dalam instruksi, sedikit cache yang terlewat, ...), semakin besar kemungkinan Anda tidak akan mendapatkan keuntungan dari lebih banyak inti yang digunakan (dengan menggunakan lebih banyak proses pekerja) atau bahkan mengalami penurunan kinerja.

Jelas itu juga tergantung pada apa lagi yang dijalankan sistem Anda, karena sistem Anda mencoba memberikan setiap utas (sebagai unit eksekusi sebenarnya dari suatu proses) dalam sistem bagian waktu berjalan yang adil pada inti yang tersedia. Jadi tidak mungkin ada generalisasi dalam hal berapa banyak pekerja yang harus Anda gunakan. Tetapi jika, misalnya, Anda memiliki loop yang ketat dan sistem Anda tidak aktif, titik awal yang baik untuk pengoptimalan adalah

os.cpu_count() // 2 # same as mp.cpu_count() // 2 

... dan meningkat dari sana.

Bagaimana @Frank Yellin sudah disebutkan, multiprocessing.Poolmenggunakan os.cpu_count()jumlah pekerja sebagai default.

os.sched_getaffinity(pid)

os.sched_getaffinity(pid)

Kembalikan set CPU, proses dengan PID pid (atau proses saat ini jika nol) dibatasi.

Sekarang core / cpu / processor / -affinity adalah tentang di mana inti beton (virtual) utas Anda (dalam proses pekerja Anda) diizinkan untuk dijalankan. OS Anda memberi setiap inti sebuah id, dari 0 hingga (jumlah-inti - 1) dan mengubah afinitas memungkinkan pembatasan ("penyematan") di mana inti aktual thread tertentu diizinkan untuk dijalankan sama sekali.

Setidaknya di Linux saya menemukan ini berarti bahwa jika tidak ada inti yang diizinkan saat ini tersedia, utas proses anak tidak akan berjalan, bahkan jika inti lain yang tidak diizinkan akan menganggur. Jadi "afinitas" agak menyesatkan di sini.

Tujuan saat mengutak-atik afinitas adalah untuk meminimalkan pembatalan cache dari sakelar konteks dan migrasi inti. OS Anda di sini biasanya memiliki wawasan yang lebih baik dan sudah mencoba untuk menjaga cache tetap "panas" dengan kebijakan penjadwalannya, jadi kecuali Anda tahu apa yang Anda lakukan, Anda tidak dapat mengharapkan keuntungan mudah dari campur tangan.

Secara default afinitas diatur ke semua inti dan untuk multiprocessing.Pool, tidak terlalu masuk akal untuk repot-repot mengubahnya, setidaknya jika sistem Anda menganggur sebaliknya.

Perhatikan bahwa meskipun dokumen di sini berbicara tentang "proses", menyetel afinitas sebenarnya adalah hal per utas. Jadi misalnya, menyetel afinitas dalam "anak" -rut untuk "proses saat ini jika nol", tidak mengubah afinitas utas utama atau utas lain dalam proses. Namun , utas anak mewarisi afinitasnya dari utas utama dan proses anak (melalui utas utama) mewarisi afinitas dari utas utama proses induk. Ini mempengaruhi semua metode start yang mungkin ("spawn", "fork", "forkserver"). Contoh di bawah ini mendemonstrasikan ini dan bagaimana mengubah afinitas dengan menggunakan multiprocessing.Pool.

import multiprocessing as mp
import threading
import os


def _location():
    return f"{mp.current_process().name} {threading.current_thread().name}"


def thread_foo():
    print(f"{_location()}, affinity before change: {os.sched_getaffinity(0)}")
    os.sched_setaffinity(0, {4})
    print(f"{_location()}, affinity after change: {os.sched_getaffinity(0)}")


def foo(_, iterations=200e6):

    print(f"{_location()}, affinity before thread_foo:"
          f" {os.sched_getaffinity(0)}")

    for _ in range(int(iterations)):  # some dummy computation
        pass

    t = threading.Thread(target=thread_foo)
    t.start()
    t.join()

    print(f"{_location()}, affinity before exit is unchanged: "
          f"{os.sched_getaffinity(0)}")

    return _


if __name__ == '__main__':

    mp.set_start_method("spawn")  # alternatives on Unix: "fork", "forkserver"

    # for current process, exclude cores 0,1 from affinity-mask
    print(f"parent affinity before change: {os.sched_getaffinity(0)}")
    excluded_cores = {0, 1}
    os.sched_setaffinity(0, os.sched_getaffinity(0).difference(excluded_cores))
    print(f"parent affinity after change: {os.sched_getaffinity(0)}")

    with mp.Pool(2) as pool:
        pool.map(foo, range(5))

Keluaran:

parent affinity before change: {0, 1, 2, 3, 4, 5, 6, 7}
parent affinity after change: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-1 MainThread, affinity before thread_foo: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-2 MainThread, affinity before thread_foo: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-1 Thread-1, affinity before change: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-1 Thread-1, affinity after change: {4}
SpawnPoolWorker-1 MainThread, affinity before exit is unchanged: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-1 MainThread, affinity before thread_foo: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-2 Thread-1, affinity before change: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-2 Thread-1, affinity after change: {4}
SpawnPoolWorker-2 MainThread, affinity before exit is unchanged: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-2 MainThread, affinity before thread_foo: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-2 Thread-2, affinity before change: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-2 Thread-2, affinity after change: {4}
SpawnPoolWorker-2 MainThread, affinity before exit is unchanged: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-2 MainThread, affinity before thread_foo: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-1 Thread-2, affinity before change: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-1 Thread-2, affinity after change: {4}
SpawnPoolWorker-1 MainThread, affinity before exit is unchanged: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-2 Thread-3, affinity before change: {2, 3, 4, 5, 6, 7}
SpawnPoolWorker-2 Thread-3, affinity after change: {4}
SpawnPoolWorker-2 MainThread, affinity before exit is unchanged: {2, 3, 4, 5, 6, 7}
1 FrankYellin Oct 04 2020 at 05:10

Penerapan penggunaan multiprocessing.pool

        if processes is None:
            processes = os.cpu_count() or 1

Tidak yakin apakah itu menjawab pertanyaan Anda, tapi setidaknya itu titik data.