Menjalankan A @ B untuk matriks besar terasa hampir instan, padahal operasi itu memuat jutaan perkalian skalar. Kecepatan itu berasal dari satu fakta arsitektur: NumPy tidak mengeksekusi loop perkalian matriks sendiri. Pekerjaan tersebut diserahkan ke BLAS (Basic Linear Algebra Subprograms) — library native yang telah dioptimalkan bertahun-tahun untuk arsitektur CPU modern.
Build NumPy pada lingkungan ini dikompilasi bersama OpenBLAS, dan OpenBLAS berjalan multi-thread secara otomatis: satu operasi matriks besar dibagi ke beberapa thread sesuai jumlah core. Bab ini membuktikan keberadaan BLAS lewat konfigurasi build, menghitung thread yang sedang dipakai dengan threadpoolctl, lalu mengendalikannya lewat variabel lingkungan — termasuk jebakan oversubscription pada container.
Operasi yang Diserahkan ke BLAS #
Perkalian dua matriks float64 dua dimensi diarahkan ke rutin GEMM milik BLAS. Kita mulai dari operasi yang dimaksud:
import numpy as np
A = np.ones((400, 300))
B = np.ones((300, 400))
C = A @ B
print(C.shape, C.dtype)
print("C[0, 0] =", C[0, 0])
print("np.dot sama:", np.allclose(C, np.dot(A, B)))Output yang Dihasilkan:
(400, 400) float64
C[0, 0] = 300.0
np.dot sama: True- Baris 5 (
C = A @ B): perkalian 400x300 @ 300x400 memuat 48 juta perkalian skalar. Untuk ukuran sebesar ini, NumPy meneruskan pekerjaan ke BLAS alih-alih menghitung dengan loop Python maupun loop C miliknya sendiri. - Baris 7 (
C[0, 0]): hasil 300.0 — jumlah 300 elemen satu, cocok dengan definisi perkalian matriks. - Baris 8 (
np.dot sama):@dannp.dotberakhir di jalur yang sama, sehingga hasilnya setara.
Karena pekerjaan dijalankan OpenBLAS yang multi-thread, operasi ini selesai cepat tanpa satu baris kode threading pun yang kita tulis.
Membaca Konfigurasi Build #
Perintah np.show_config() mencetak laporan build secara lengkap, tetapi outputnya panjang sehingga tidak nyaman dipanggil utuh di sini. Seluruh isinya tersedia sebagai dictionary di np.__config__.CONFIG, dan dari situ kita cukup mengambil bagian yang penting:
import numpy as np
kunci = sorted(np.__config__.CONFIG.keys())
print(kunci)
dep = np.__config__.CONFIG["Build Dependencies"]
print("blas :", dep["blas"]["name"], dep["blas"]["version"])
print("lapack:", dep["lapack"]["name"], dep["lapack"]["version"])Output yang Dihasilkan:
['Build Dependencies', 'Compilers', 'Machine Information', 'Python Information', 'SIMD Extensions']
blas : scipy-openblas 0.3.27
lapack: scipy-openblas 0.3.27- Baris 3-4 (
kunci):CONFIGmemuat lima kelompok informasi — Build Dependencies, Compilers, Machine Information, Python Information, dan SIMD Extensions. Fungsishow_config()pada dasarnya hanya merapikan dictionary ini menjadi laporan teks. - Baris 5-7 (
dep): bagian Build Dependencies menunjuk dua library:blasdanlapack. Keduanya terisiscipy-openblas 0.3.27— bukti bahwa build ini memakai OpenBLAS.
Menghitung Thread dengan threadpoolctl #
Jumlah thread yang sedang dipakai OpenBLAS dapat dibaca lewat paket threadpoolctl (versi 3.6.0 pada lingkungan ini). Urutannya penting: import NumPy lebih dulu agar library native termuat, baru panggil threadpool_info:
import numpy as np
from threadpoolctl import threadpool_info
for info in threadpool_info():
print(info["internal_api"], "-", info["num_threads"], "thread")Output yang Dihasilkan:
openblas - 2 threadinternal_api berisi openblas, dan num_threads bernilai 2 — sama dengan jumlah core pada mesin ini. OpenBLAS secara bawaan memakai seluruh core yang terdeteksi, sehingga angka ini akan mengikuti mesin tempat kode dijalankan. Library lain yang dimuat (misal SciPy atau scikit-learn) akan menambah entri baru pada daftar yang dikembalikan threadpool_info.
Mengendalikan Thread lewat Variabel Lingkungan #
Variabel OPENBLAS_NUM_THREADS dan OMP_NUM_THREADS menentukan banyaknya thread BLAS. Keduanya dibaca satu kali, yaitu saat library native dimuat — dan import numpy justru memuat OpenBLAS. Maka pengaturan harus dipasang sebelum import NumPy:
# notest
# Di terminal, variabel terpasang sebelum proses Python dimulai:
# OPENBLAS_NUM_THREADS=1 python3 analisis.py
import os
os.environ["OPENBLAS_NUM_THREADS"] = "1" # alternatif: di awal skrip
import numpy as np
from threadpoolctl import threadpool_info
print(threadpool_info()[0]["num_threads"])Output yang Dihasilkan:
1Cara yang paling umum adalah lewat terminal, sehingga variabel sudah terpasang sebelum proses Python dimulai:
OPENBLAS_NUM_THREADS=1 python3 analisis.py| Variabel lingkungan | Dibaca oleh | Catatan |
|---|---|---|
OPENBLAS_NUM_THREADS | OpenBLAS | paling spesifik untuk build ini |
GOTO_NUM_THREADS | OpenBLAS (legasi) | prioritas di bawah OPENBLAS_NUM_THREADS |
OMP_NUM_THREADS | runtime OpenMP | dipakai bila dua variabel di atas kosong |
Jebakan Oversubscription di Container #
Container produksi umumnya ber-core sedikit tetapi menjalankan banyak proses. Contoh: sebuah scheduler membagi tugas ke 8 worker Python di container ber-2 core. Setiap worker yang mengimpor NumPy otomatis membuka 2 thread OpenBLAS — total 16 thread berebut 2 core. Waktu CPU habis untuk pergantian konteks antar thread, dan seluruh pekerjaan malah melambat.
Aturan praktisnya: jumlah total thread aktif sebaiknya mendekati jumlah core. Untuk banyak worker, set OPENBLAS_NUM_THREADS=1 di tiap worker (atau total core dibagi banyaknya worker). Kapan pun ragu, ukur sendiri dengan pola bab Mengukur Waktu Eksekusi.
Ada satu situasi lagi ketika satu thread justru menang: matriks kecil. Memecah pekerjaan kecil ke beberapa thread menambah biaya sinkronisasi — membagi tugas, menggabungkan hasil — yang bisa melebihi keuntungan paralelnya. Pada beban berupa banyak operasi kecil yang berulang, mode satu thread sering lebih cepat dan jauh lebih stabil waktunya. Batasan seperti ini menegaskan pesan bab Kapan NumPy Bukan Pilihan Terbaik: pengaturan lingkungan hanya berguna bila hambatannya memang berada di komputasi numerik, bukan di overhead Python.