BLAS: Level 1, 2, 3
BLAS (Basic Linear Algebra Subprograms) adalah kosakata standar operasi aljabar linear padat (dense): perkalian vektor, matriks-vektor, matriks-matriks, dan kawan-kawannya. Hampir semua yang ada di atasnya — LAPACK, NumPy, PyTorch, HPL — pada akhirnya memanggil BLAS. Pembagian Level 1, 2, 3 awalnya cuma urutan sejarah, tapi kebetulan (dan ini bagian pentingnya) membelah dunia berdasarkan arithmetic intensity: Level 1–2 selamanya terkurung bandwidth memori, Level 3 satu-satunya yang bisa menyentuh puncak FLOPS mesin.
Prasyarat enak: GFLOPS dan Roofline model.
Tiga Level
| Tahun | Operasi | Contoh | Flops | Data | Intensitas | Terikat oleh | |
|---|---|---|---|---|---|---|---|
| Level 1 | 1979 | vektor–vektor | axpy, dot |
O(n) | O(n) | ~0.08 flop/byte | bandwidth |
| Level 2 | 1988 | matriks–vektor | gemv, trsv |
O(n²) | O(n²) | ~0.25 flop/byte | bandwidth |
| Level 3 | 1990 | matriks–matriks | gemm, trsm |
O(n³) | O(n²) | ~n/16 flop/byte | compute |
Logika intensitasnya:
- Level 1 —
daxpy(y ← αx + y): tiap elemen dibaca sekali, dipakai untuk 2 flop, selesai. 2n flop untuk ~24n byte lalu lintas → 0.083 flop/byte. Mau prosesor secepat apa pun, kecepatannya = kecepatan RAM. - Level 2 —
dgemv(y ← αAx + βy): matriks A (8n² byte) dibaca sekali untuk 2n² flop → 0.25 flop/byte. Masih nasib yang sama. - Level 3 —
dgemm(C ← αAB + βC): 2n³ flop atas data hanya O(n²) — tiap elemen A dan B dipakai n kali. Dengan blocking yang benar, data bisa parkir di cache dan dipakai berulang; intensitas naik sebanding n. Inilah satu-satunya operasi BLAS yang bisa lari di atap compute roofline.
Satu kalimat yang layak dihafal: "Level 3 bukan sekadar lebih banyak kerja — dia kerja yang bisa di-cache."
Membaca Nama Rutin
Nama BLAS kelihatan seperti kucing jalan di keyboard (dgemm, ztrsv) padahal sistematis: presisi + jenis matriks + operasi.
| Bagian | Arti |
|---|---|
s / d / c / z |
float / double / complex / double complex |
ge / sy / he / tr / gb |
general / symmetric / hermitian / triangular / general banded |
mm / mv / sv / rk |
matrix-matrix / matrix-vector / solve / rank-k update |
Jadi dgemm = double general matrix-matrix multiply, dan ztrsv = double-complex triangular solve dengan vektor. Rutin yang paling sering ketemu:
| Level | Rutin | Rumus |
|---|---|---|
| 1 | daxpy |
y ← αx + y |
| 1 | ddot |
hasil ← xᵀy |
| 1 | dscal, dnrm2 |
x ← αx; ‖x‖₂ |
| 2 | dgemv |
y ← αAx + βy |
| 2 | dger |
A ← αxyᵀ + A (rank-1 update) |
| 2 | dtrsv |
selesaikan Tx = b, T segitiga |
| 3 | dgemm |
C ← αAB + βC (op(A), op(B) bisa transpose) |
| 3 | dtrsm |
selesaikan TX = B untuk banyak kolom sekaligus |
| 3 | dsyrk |
C ← αAAᵀ + βC |
Warisan Fortran: Column-Major & Leading Dimension
BLAS lahir dari Fortran, jadi API aslinya berasumsi matriks tersimpan column-major (kolom demi kolom di memori). C/C++ terbiasa row-major. Interface C-nya, CBLAS (<cblas.h>), memberi parameter pertama CblasRowMajor/CblasColMajor supaya tidak perlu mentranspose kepala sendiri.
Parameter yang paling sering bikin bingung: leading dimension (lda, ldb, ldc) = jarak (dalam elemen) antara awal baris berturut-turut (row-major) atau kolom berturut-turut (col-major) di memori. Untuk matriks utuh, lda = lebar alokasinya. Gunanya: mengoperasikan submatriks tanpa menyalin —
// A_besar berukuran M x N (row-major). Kita mau blok m x k yang
// pojok kirinya di (bi, bj): cukup geser pointer + lda tetap N.
cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,
m, n_blok, k,
1.0, A_besar + bi * N + bj, N, // lda = N, BUKAN k!
B, ldb, 0.0, C, ldc);
Salah mengisi lda tidak selalu crash — sering cuma hasil salah diam-diam. Kalau hasil gemm terlihat seperti sampah terstruktur, cek urutan major dan leading dimension dulu.
Mengukur Ketiga Level
Program lengkap: satu operasi wakil tiap level, ukur GFLOPS-nya. Compile:
g++ -O2 -fopenmp -o blas_level blas_level.cpp -lcblas
#include <cstdio>
#include <cblas.h>
#include <omp.h>
// Ukur GFLOPS satu operasi wakil tiap level BLAS.
// Level 1: daxpy (2n flops, ~24n byte) -> intensitas ~0.08 flop/byte
// Level 2: dgemv (2n^2 flops, ~8n^2 byte) -> intensitas ~0.25 flop/byte
// Level 3: dgemm (2n^3 flops, ~32n^2 byte)-> intensitas ~n/16 flop/byte
int main() {
// ---- Level 1: y = a*x + y ----
{
const long n = 1L << 26; // 2 array x 512 MiB
double *x = new double[n], *y = new double[n];
#pragma omp parallel for schedule(static)
for (long i = 0; i < n; ++i) { x[i] = 1.0; y[i] = 2.0; }
double best = 1e99;
for (int u = 0; u < 5; ++u) {
double t0 = omp_get_wtime();
cblas_daxpy(n, 3.0, x, 1, y, 1);
double t1 = omp_get_wtime();
if (t1 - t0 < best) best = t1 - t0;
}
printf("L1 daxpy n=%-9ld : %7.2f GFLOPS (%.1f GB/s)\n",
n, 2.0 * n / best / 1e9, 24.0 * n / best / 1e9);
delete[] x; delete[] y;
}
// ---- Level 2: y = A*x ----
{
const long n = 8192; // A = 512 MiB
double *A = new double[n * n], *x = new double[n], *y = new double[n];
#pragma omp parallel for schedule(static)
for (long i = 0; i < n * n; ++i) A[i] = 1e-3;
for (long i = 0; i < n; ++i) { x[i] = 1.0; y[i] = 0.0; }
double best = 1e99;
for (int u = 0; u < 5; ++u) {
double t0 = omp_get_wtime();
cblas_dgemv(CblasRowMajor, CblasNoTrans, n, n,
1.0, A, n, x, 1, 0.0, y, 1);
double t1 = omp_get_wtime();
if (t1 - t0 < best) best = t1 - t0;
}
printf("L2 dgemv n=%-9ld : %7.2f GFLOPS (%.1f GB/s)\n",
n, 2.0 * n * n / best / 1e9, 8.0 * n * n / best / 1e9);
delete[] A; delete[] x; delete[] y;
}
// ---- Level 3: C = A*B ----
{
const long n = 1024;
double *A = new double[n * n], *B = new double[n * n], *C = new double[n * n];
for (long i = 0; i < n * n; ++i) { A[i] = 1e-3; B[i] = 1e-3; C[i] = 0.0; }
double best = 1e99;
for (int u = 0; u < 3; ++u) {
double t0 = omp_get_wtime();
cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans, n, n, n,
1.0, A, n, B, n, 0.0, C, n);
double t1 = omp_get_wtime();
if (t1 - t0 < best) best = t1 - t0;
}
printf("L3 dgemm n=%-9ld : %7.2f GFLOPS\n", n, 2.0 * n * n * n / best / 1e9);
delete[] A; delete[] B; delete[] C;
}
return 0;
}
Hasil nyata (i5-13450HX, paket Arch cblas = reference BLAS dari Netlib):
L1 daxpy n=67108864 : 2.47 GFLOPS (29.6 GB/s) L2 dgemv n=8192 : 2.93 GFLOPS (11.7 GB/s) L3 dgemm n=1024 : 3.77 GFLOPS
Bacaannya:
- daxpy: 29.6 GB/s ≈ bandwidth memori mesin ini (bandingkan triad ~25 GB/s di OpenMP: Affinity & First-Touch). Level 1 sudah mentok di atap bandwidth — tidak ada implementasi yang bisa membuatnya jauh lebih cepat, fisika sudah bicara.
- dgemv setali tiga uang, terikat laju pembacaan A.
- dgemm: 3.77 GFLOPS. Nah ini skandal. Level 3 harusnya compute-bound — mesin ini sanggup ratusan GFLOPS — tapi cuma dapat 4-an. Kenapa? Lanjut ke bagian berikut.
BLAS Adalah Interface, Bukan Library
BLAS hanya mendefinisikan apa fungsinya; seberapa cepat sepenuhnya urusan implementasi:
| Implementasi | Catatan |
|---|---|
| Reference (Netlib) | Fortran lurus, tanpa blocking/SIMD/threading. Tolok ukur kebenaran, bukan kecepatan. Di Arch ini paket blas/cblas.
|
| OpenBLAS | Open source, assembly kernel per arsitektur, multithread. Standar de-facto di Linux. (Arch: openblas / blas-openblas.)
|
| Intel MKL | Tercepat di CPU Intel; gratis dipakai. |
| BLIS | Framework riset modern, basis AMD AOCL. |
| cuBLAS | NVIDIA GPU — dipanggil dari host, kerjanya di device. |
Bukti bahwa implementasi adalah segalanya — lomba dgemm n=1024, empat kandidat:
// 1. Tiga loop polos, urutan ijk (inner loop melompati B selebar n)
void naive_ijk(const double *A, const double *B, double *C) {
for (long i = 0; i < n; ++i)
for (long j = 0; j < n; ++j) {
double s = 0.0;
for (long k = 0; k < n; ++k)
s += A[i * n + k] * B[k * n + j];
C[i * n + j] = s;
}
}
// 2. Loop yang sama, urutan ikj (inner loop kontigu -> SIMD & cache jalan)
void reorder_ikj(const double *A, const double *B, double *C) {
for (long i = 0; i < n; ++i)
for (long k = 0; k < n; ++k) {
const double a = A[i * n + k];
for (long j = 0; j < n; ++j)
C[i * n + j] += a * B[k * n + j];
}
}
// 3. ikj + OpenMP di loop terluar
void omp_ikj(const double *A, const double *B, double *C) {
#pragma omp parallel for schedule(static)
for (long i = 0; i < n; ++i)
for (long k = 0; k < n; ++k) {
const double a = A[i * n + k];
for (long j = 0; j < n; ++j)
C[i * n + j] += a * B[k * n + j];
}
}
// 4. Serahkan ke profesional
void blas_dgemm(const double *A, const double *B, double *C) {
cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans, n, n, n,
1.0, A, n, B, n, 0.0, C, n);
}
Hasil nyata (16 thread):
n = 1024, threads = 16 naive ijk : 3.294 s 0.65 GFLOPS reorder ikj : 0.436 s 4.92 GFLOPS ikj + OpenMP : 0.048 s 45.03 GFLOPS cblas_dgemm (ref) : 0.406 s 5.29 GFLOPS
Empat pelajaran dari satu tabel:
- Urutan loop = 7.6×.
ijk→ikjmengubah akses B dari lompat-lompat selebar baris jadi kontigu: Cache line terpakai penuh dan compiler berani auto-vectorize (SIMD). Belum ada paralelisme sama sekali. - OpenMP menambah 9× lagi di 16 thread.
- Kode 20 baris kita mengalahkan BLAS terinstall 8.5×. Bukan karena kita hebat — karena yang terinstall reference BLAS. Ini jebakan nyata: di banyak distro dan cluster,
-lblasdefault menaut ke Netlib. Selalu cek implementasi apa yang sebenarnya ke-link (ldd ./program). - 45 GFLOPS itu tetap kelas amatir. OpenBLAS/MKL di mesin yang sama tipikalnya ratusan GFLOPS (puncak teoretis mesin ini ~400+ GFLOPS DP) lewat blocking bertingkat mengikuti hierarki cache + microkernel assembly + packing. Teknik blocking-nya persis ide yang sama dengan CUDA: Matmul dengan Tiling — beda alamat, ide serupa.
Moral: jangan pernah menulis gemm sendiri untuk produksi. Tulis sekali untuk belajar (seperti di atas), lalu panggil library selamanya.
Kenapa Level 3 Itu Raja
Karena hanya Level 3 yang bisa lari di atap compute, seni menyusun algoritma numerik = memindahkan sebanyak mungkin kerja ke Level 3:
- LAPACK menyusun ulang faktorisasi (Dekomposisi LU & Cholesky) jadi versi blocked: panel difaktorkan dengan Level 2 (porsi kecil), lalu sisanya di-update besar-besaran via
dgemm/dtrsm(porsi raksasa). Rasio kerja Level 3-nya mendekati 100% saat matriks besar. - Deep learning duduk di atas
gemm: layer fully-connected = gemm, convolution di-lower jadi gemm (im2col), attention = gemm bertumpuk. GPU tensor core pun pada dasarnya mesin gemm. - Pola praktis: kalau kode memanggil
gemvberulang dengan matriks yang sama, tumpuk vektor-vektornya jadi matriks dan panggilgemmsekali. Kerjanya (flop) sama persis; lalu lintas memorinya runtuh karena A dibaca sekali, bukan k kali.
Catatan batas wilayah: semua ini dunia dense. Begitu matriksnya sparse (mayoritas nol — tipikal PDE, graf), format dan pustakanya beda total: Sparse Matrix: Format CSR & CSC dan SpMV — yang sayangnya kembali bernasib Level-2: memory-bound.
Jebakan Umum
- Menaut reference BLAS tanpa sadar. Gejala: gemm "cuma" beberapa GFLOPS (terbukti di atas). Diagnosa:
ldd ./program | grep blas. Obat di Arch: pasangopenblas+ link-lopenblas(atau paketblas-openblasyang menggantikan providerlibblas.sosekalian). - Row-major vs column-major kebalik. Hasilnya transpose diam-diam. Pakai CBLAS dan konsisten
CblasRowMajor, atau manfaatkan identitas (AB)ᵀ = BᵀAᵀ. - Perang thread. OpenBLAS punya thread pool sendiri. Memanggil BLAS dari dalam region OpenMP = oversubscription (16×16 thread). Aturan: BLAS multithread di luar region paralel, atau set
OPENBLAS_NUM_THREADS=1saat dipanggil dari dalam. - Matriks kecil. Overhead pemanggilan + setup mengalahkan kerjanya untuk n ≲ 50-an; kalau punya ribuan matriks kecil, cari batched BLAS (
cublasDgemmBatched, MKL batch API), jangan loop. - Lupa β.
dgemmmenghitung C ← αAB + βC. β=0 menimpa C (dan tidak membaca isinya), β=1 mengakumulasi. Salah pilih = bug hasil yang halus. - Increment (incx) eksotis.
daxpydkk. menerima stride antar elemen. Berguna untuk kolom di matriks row-major, tapi stride besar = tiap elemen satu cache line = bandwidth efektif ambruk 8×.
Ringkasan
- Level = kelas intensitas aritmetika: L1 (vektor, 0.08 flop/byte) dan L2 (matriks-vektor, 0.25) terkurung bandwidth; hanya L3 (matriks-matriks, ~n/16) yang bisa menyentuh puncak FLOPS.
- Nama rutin = presisi + jenis matriks + operasi:
dgemm= double, general, matrix-matrix. - BLAS itu interface. Reference Netlib untuk kebenaran; OpenBLAS/MKL/cuBLAS untuk kecepatan. Cek
ldd, jangan berasumsi. - Terbukti di mesin sendiri: loop order 7.6×, +OpenMP 9×, dan reference BLAS kalah dari 20 baris kode — tapi implementasi serius masih ~8× di atasnya lagi.
- Strategi algoritma numerik modern: dorong semua kerja ke Level 3.
Lihat Juga
- GFLOPS dan Roofline model — kerangka membaca angka-angka di atas
- Cache line dan SIMD — kenapa urutan loop berdampak 7.6×
- Dekomposisi LU & Cholesky — konsumen terbesar Level 3
- Sparse Matrix: Format CSR & CSC, SpMV — dunia sebelah, saat matriks kebanyakan nol
- CUDA: Matmul dengan Tiling — blocking yang sama, di GPU
- Benchmark: HPL & HPCG — HPL pada dasarnya kontes dgemm sedunia
- OpenMP: Parallel For & Scheduling — dipakai di versi 45 GFLOPS di atas