এই পর্বে যা শিখবেন
- Amdahl আর Gustafson-এর সূত্র দিয়ে speedup ও efficiency হিসাব, আর strong বনাম weak scaling-এর পার্থক্য
- Flynn-এর চার শ্রেণি উদাহরণসহ, SPMD ধাঁচ, আর shared memory বনাম message passing (thread/OpenMP বনাম MPI)
- UMA, NUMA, ccNUMA; first-touch নীতি; আর directory coherence-এর খরচ ও ৩-hop read miss
- Bus, crossbar, ring, mesh, torus, hypercube, fat tree — diameter, degree, link আর bisection bandwidth-এর হিসাব; hypercube-এ bit-fixing routing
- Cluster, warehouse-scale computer, supercomputer; roofline model দিয়ে একটি kernel-এর সীমা বের করা; big.LITTLE আর heterogeneous chip
১ · কেন সমান্তরালতা — power wall, Amdahl আর Gustafson
প্রায় ২০০৫ সাল পর্যন্ত প্রোগ্রাম দ্রুত চালানোর সহজ উপায় ছিল অপেক্ষা করা — পরের বছরের CPU-র clock বেশি, তাই একই প্রোগ্রাম আপনাআপনি দ্রুত। তারপর সেই রাস্তা বন্ধ হয়। পর্ব ০২-এ দেখেছেন, transistor ছোট হলেও voltage আর কমানো যাচ্ছিল না (Dennard scaling-এর অবসান)। CMOS-এর dynamic power মোটামুটি C·V²·f (পর্ব ০৮) — V না কমিয়ে f বাড়ালে chip গরম হয়ে যায়। এই দেয়ালের নাম power wall. শিল্পের উত্তর ছিল: একটি দ্রুত core-এর বদলে কয়েকটি মাঝারি core. কিন্তু তাতে প্রোগ্রাম আপনাআপনি দ্রুত হয় না — প্রোগ্রামকেই সমান্তরাল হতে হয়।
বিয়েবাড়ির রান্না ভাবুন। একজন বাবুর্চি ৫০০ জনের বিরিয়ানি রাঁধতে যদি ১০ ঘণ্টা নেন, দশজন বাবুর্চি কি এক ঘণ্টায় পারবেন? পেঁয়াজ কাটা, মাংস মাখানো — এসব ভাগ করা যায়। কিন্তু ডেকচিতে দম দেওয়ার সময়টুকু কমে না, মশলার মাপ একজনকেই ঠিক করতে হয়। এই “ভাগ করা যায় না” অংশটুকুই সমান্তরালতার সীমা টানে। আবার উল্টো দিক থেকেও ভাবা যায়: দশজন বাবুর্চি থাকলে একই ১০ ঘণ্টায় হয়তো ৫০০ নয়, ৫০০০ জনের রান্না হয়ে যায়।
Amdahl-এর সূত্র আবার — strong scaling
পর্ব ০৩-এর Amdahl's law-কে এবার core-এর ভাষায় লিখি। একটি প্রোগ্রাম একটি core-এ চলতে T সময় নেয়। তার s অংশ serial (ভাগ করা যায় না), বাকি (১ − s) N টি core-এ নিখুঁতভাবে ভাগ হয়। তাহলে N core-এ সময় = T·(s + (১ − s)/N), আর
Amdahl: S(N) = 1 / (s + (1 − s)/N), সীমা 1/sএখানে কাজের আকার স্থির — একই সমস্যা, বেশি core. এই মাপকে বলে strong scaling. প্রতি core কতটা কাজে লাগছে, তা বলে parallel efficiency E = S/N. s = ০.০৫ (মাত্র ৫% serial) আর N = ১৬ হলে S = ১ / (০.০৫ + ০.৯৫/১৬) = ১ / ০.১০৯৪ ≈ ৯.১, অর্থাৎ efficiency মাত্র ৫৭%.
Gustafson-এর সূত্র — weak scaling
১৯৮৮ সালে John Gustafson প্রশ্নটি উল্টে দেন: বাস্তবে বড় যন্ত্র পেলে মানুষ একই ছোট সমস্যা দ্রুত শেষ করে না, বড় সমস্যা চালায় — আবহাওয়ার মডেলে আরও সূক্ষ্ম grid, ছবিতে আরও pixel. তাই N core-এ চলা প্রোগ্রামের সময়কে ১ ধরে মাপুন: তার s অংশ serial, (১ − s) অংশ N টি core-এ সমান্তরাল। একই কাজ একটি core-এ করলে লাগত s + (১ − s)·N. অনুপাতই Gustafson's law:
Gustafson: S(N) = s + (1 − s)·N = N − s·(N − 1)Amdahl-এর s মাপা হয় একটি core-এর run-এ, Gustafson-এর s মাপা হয় N core-এর run-এ। দুটি সূত্র তাই একে অপরের বিরোধী নয় — দুটি আলাদা প্রশ্নের উত্তর। প্রশ্ন যদি হয় “এই নির্দিষ্ট কাজটি কত দ্রুত শেষ হবে?” — Amdahl. প্রশ্ন যদি হয় “একই সময়ে কত বড় কাজ করা যাবে?” — Gustafson. পরীক্ষায় কোন সূত্র লাগবে, তা প্রশ্নের এই ভাষা দেখেই ঠিক করুন।
নিচের calculator চারটি serial ভগ্নাংশ (১%, ৫%, ১০%, ২৫%) আর ১ থেকে ১০২৪ core-এর জন্য দুই সূত্রের speedup আর efficiency ছাপে। Run চাপুন:
// Amdahl (fixed problem, strong scaling) vs Gustafson (scaled problem, weak scaling)
const amdahl = (s, N) => 1 / (s + (1 - s) / N);
const gustafson = (s, N) => s + (1 - s) * N;
const S = [0.01, 0.05, 0.10, 0.25];
const NS = [1, 4, 16, 64, 256, 1024];
const pct = e => (e < 0.01 ? "<1" : String(Math.round(100 * e))) + "%";
const cell = (sp, N) => (sp.toFixed(1) + " (" + pct(sp / N) + ")").padStart(14);
function table(name, f) {
console.log(name + " speedup (efficiency)");
console.log("N".padStart(5) + S.map(s => ("s=" + s.toFixed(2)).padStart(14)).join(""));
for (const N of NS)
console.log(String(N).padStart(5) + S.map(s => cell(f(s, N), N)).join(""));
console.log("");
}
table("AMDAHL (same problem, more cores)", amdahl);
table("GUSTAFSON (problem grows with cores)", gustafson);
// Amdahl efficiency S/N >= 50% <=> s(N - 1) <= 1 <=> N <= 1 + 1/s
for (const s of S)
console.log("s=" + s.toFixed(2) + ": ceiling 1/s = " + (1 / s).toFixed(0) +
", efficiency >= 50% only while N <= " + (1 + 1 / s).toFixed(0));আউটপুট (নির্ধারিত — প্রতিবার একই):
AMDAHL (same problem, more cores) speedup (efficiency)
N s=0.01 s=0.05 s=0.10 s=0.25
1 1.0 (100%) 1.0 (100%) 1.0 (100%) 1.0 (100%)
4 3.9 (97%) 3.5 (87%) 3.1 (77%) 2.3 (57%)
16 13.9 (87%) 9.1 (57%) 6.4 (40%) 3.4 (21%)
64 39.3 (61%) 15.4 (24%) 8.8 (14%) 3.8 (6%)
256 72.1 (28%) 18.6 (7%) 9.7 (4%) 4.0 (2%)
1024 91.2 (9%) 19.6 (2%) 9.9 (<1%) 4.0 (<1%)
GUSTAFSON (problem grows with cores) speedup (efficiency)
N s=0.01 s=0.05 s=0.10 s=0.25
1 1.0 (100%) 1.0 (100%) 1.0 (100%) 1.0 (100%)
4 4.0 (99%) 3.8 (96%) 3.7 (93%) 3.3 (81%)
16 15.8 (99%) 15.3 (95%) 14.5 (91%) 12.3 (77%)
64 63.4 (99%) 60.8 (95%) 57.7 (90%) 48.3 (75%)
256 253.4 (99%) 243.3 (95%) 230.5 (90%) 192.3 (75%)
1024 1013.8 (99%) 972.8 (95%) 921.7 (90%) 768.3 (75%)
s=0.01: ceiling 1/s = 100, efficiency >= 50% only while N <= 101
s=0.05: ceiling 1/s = 20, efficiency >= 50% only while N <= 21
s=0.10: ceiling 1/s = 10, efficiency >= 50% only while N <= 11
s=0.25: ceiling 1/s = 4, efficiency >= 50% only while N <= 5
দুই টেবিলের তফাত চোখে পড়ার মতো। Amdahl-এ s = ০.০৫ হলে ৬৪ core-এ speedup ১৫.৪ (efficiency ২৪%), ১০২৪ core-এ ১৯.৬ — ছাদ ২০-এর একেবারে কাছে, অর্থাৎ ৯৬০টি বাড়তি core প্রায় কিছুই দেয়নি। Gustafson-এ একই s-এ ১০২৪ core-এ speedup ৯৭২.৮, efficiency ৯৫% — কারণ সমস্যা বড় হওয়ায় serial অংশের ভাগ আর বাড়েনি। শেষ চার লাইন একটি কাজের নিয়ম দেয়: Amdahl-এ efficiency ≥ ৫০% রাখতে হলে N ≤ ১ + ১/s (প্রমাণ: S/N = ১ / (sN + ১ − s) ≥ ১/২ ⇔ s(N − ১) ≤ ১). ৫% serial কোডের জন্য ২১টির বেশি core দিলে প্রতিটি core অর্ধেকের বেশি সময় অলস।
বাস্তবে দুটি সূত্রই আশাবাদী — দুটিই ধরে নেয় সমান্তরাল অংশ নিখুঁতভাবে ভাগ হয় আর core-দের মধ্যে কথা বলার কোনো খরচ নেই। অংশ ৮-এ দেখব, যোগাযোগ আর synchronisation-এর খরচ N-এর সাথে বাড়ে, তাই বাস্তব strong scaling-এর গ্রাফ এক সময় নিচে নামতেও শুরু করে।
২ · Flynn-এর চার শ্রেণি — পুরো ছবি, SPMD আর সীমাবদ্ধতা
পর্ব ৪২-এ Flynn's taxonomy-র সাথে পরিচয় হয়েছে — ১৯৬৬ সালে Michael Flynn-এর প্রস্তাব: একসাথে কয়টি instruction-এর ধারা আর কয়টি ডেটার ধারা চলে? এবার চারটি ঘরই পুরো দেখি:
| শ্রেণি | ধারা (instruction × ডেটা) | উদাহরণ | মন্তব্য |
|---|---|---|---|
| SISD | ১ × ১ | ক্লাসিক von Neumann যন্ত্র, এই কোর্সের RV32I single-cycle datapath (পর্ব ৩২) | Pipeline বা superscalar হলেও প্রোগ্রামের চোখে একটি ধারা |
| SIMD | ১ × অনেক | SSE-AVX, NEON, RISC-V Vector, Cray-1, পুরোনো array processor | একটি control unit, অনেক lane; GPU-র SIMT এর কাছাকাছি (পর্ব ৫৫) |
| MISD | অনেক × ১ | বিতর্কিত: একই ইনপুট কয়েকটি আলাদা কম্পিউটারে চালিয়ে ভোট নেওয়া নিরাপত্তা-সিস্টেম; কেউ systolic array বলেন | বাস্তবে প্রায় শূন্য ঘর |
| MIMD | অনেক × অনেক | Multicore CPU, বহু-socket server, cluster, supercomputer | আজকের প্রায় সব সমান্তরাল যন্ত্র |
SPMD — MIMD যন্ত্রে প্রোগ্রাম লেখার আসল ধাঁচ
MIMD মানে প্রতিটি core আলাদা প্রোগ্রাম চালাতে পারে। কিন্তু হাজার core-এর জন্য হাজারটি আলাদা প্রোগ্রাম কেউ লেখে না। বাস্তবে প্রায় সবাই লেখে একটিই প্রোগ্রাম, যা প্রতিটি core-এ চলে নিজের ডেটার টুকরায় — আর নিজের নম্বর (rank বা thread id) দেখে ঠিক করে কোন টুকরা তার। এই ধাঁচের নাম SPMD (Single Program, Multiple Data). পরীক্ষার ক্লাসরুমে সব পরীক্ষার্থী একই প্রশ্নপত্র পান, কিন্তু প্রত্যেকে নিজের খাতায় লেখেন, আর কেউ কেউ ঐচ্ছিক প্রশ্নে আলাদা পথে যান — SPMD ঠিক এমন: একই কোড, আলাদা ডেটা, branch-এ আলাদা পথ নেওয়ার স্বাধীনতা। SIMD-এর সাথে তফাত এখানেই — SIMD-এ সব lane একই মুহূর্তে একই instruction চালায়; SPMD-তে প্রতিটি core নিজের গতিতে নিজের PC নিয়ে চলে। MPI, OpenMP আর GPU-র CUDA kernel — তিনটিই মূলত SPMD.
Flynn-এর ভাগ কোথায় থেমে যায়
প্রায় ষাট বছর পরেও Flynn-এর ভাগ পরীক্ষায় আর আলোচনায় টিকে আছে কারণ এটি সরল। কিন্তু নকশাকারের কাছে এটি অনেক কিছু বলে না। একটি ৮-core ফোনের chip একসাথে MIMD (আটটি core), SIMD (প্রতিটি core-এ NEON) আর আংশিক SISD. GPU কোন ঘরে — SIMD না MIMD? সবচেয়ে বড় ফাঁক: মেমরি কীভাবে ভাগ হয় আর core-রা কীভাবে কথা বলে — Flynn-এর ভাগ এ নিয়ে নীরব, অথচ পারফরম্যান্স আর প্রোগ্রামিং-এর কষ্ট মূলত এর ওপরই নির্ভর করে। তাই আধুনিক বইয়ে MIMD-কে আরও ভাগ করা হয়: shared memory (UMA, NUMA) আর distributed memory (message passing) — পরের দুই অংশের বিষয়।
৩ · Shared memory বনাম message passing
একটি অফিসে সবাই একই হোয়াইটবোর্ডে হিসাব লেখেন — যে কেউ যেকোনো ঘর পড়তে আর মুছতে পারেন। কথা বলা সহজ, কিন্তু দুজন একই ঘরে একসাথে লিখলে গোলমাল; তাই নিয়ম লাগে (তালা, পালা)। এটি shared memory. অন্যদিকে ঢাকা, চট্টগ্রাম আর রাজশাহীর তিনটি শাখা অফিস — প্রত্যেকের নিজের খাতা, অন্যের খাতা দেখা যায় না; কিছু জানাতে হলে চিঠি বা ইমেইল পাঠাতে হয়। এটি message passing. চিঠির খরচ আছে, কিন্তু কে কখন কী জানল তা স্পষ্ট, আর শাখা যত খুশি বাড়ানো যায়।
Shared memory-তে সব core একই address space দেখে; একটি core-এর store অন্য core-এর load-এ দেখা যায়। Hardware-কে তখন দুটি জিনিস নিশ্চিত করতে হয় — cache coherence (একই ঠিকানার সব কপি মেলানো) আর memory consistency (কোন store কোন ক্রমে দেখা যাবে); দুটিই পর্ব ৫৩-এর বিষয়। প্রোগ্রামিং-এ thread — POSIX thread বা OpenMP. একটি chip বা একটি server-এর ভেতরে এটিই প্রধান মডেল।
Message passing-এ প্রতিটি node-এর নিজের মেমরি, অন্যটির মেমরি সরাসরি পড়া যায় না। ডেটা যায় স্পষ্ট send/receive দিয়ে, network-এর ওপর দিয়ে। Hardware সরল (coherence নেই), আর হাজার হাজার node পর্যন্ত বাড়ে — তাই cluster আর supercomputer-এ এর মানদণ্ড MPI. আজকের বড় যন্ত্রে দুটিই একসাথে চলে: node-এর ভেতরে thread, node-এর বাইরে MPI (“MPI + X”).
একই কাজ — একটি array-এর যোগফল — দুই মডেলে কেমন দেখায়, নিচের দুটি ছোট উদাহরণে দেখুন (এখানে চালানো যায় না; OpenMP-র জন্য gcc -fopenmp, MPI-র জন্য একটি MPI library আর mpirun লাগে):
double sum = 0.0;
#pragma omp parallel for reduction(+:sum) /* threads share a[]; each keeps a private partial sum */
for (long i = 0; i < n; i++)
sum += a[i]; /* partial sums are combined at the end */int rank, size;
MPI_Comm_rank(MPI_COMM_WORLD, &rank); /* who am I? (SPMD: same program everywhere) */
MPI_Comm_size(MPI_COMM_WORLD, &size);
double local = 0.0, total = 0.0;
for (long i = 0; i < n_local; i++) /* my_part[] lives only in this node's memory */
local += my_part[i];
MPI_Reduce(&local, &total, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD); /* messages */OpenMP-এ a[] সবার, MPI-তে প্রতিটি rank শুধু নিজের my_part[] জানে, আর MPI_Reduce network দিয়ে আংশিক যোগফলগুলো জড়ো করে। দুটিতেই একটি কৌশল একই — প্রতিটি কর্মী আগে নিজের ব্যক্তিগত আংশিক যোগফল রাখে, শেষে একবার মেলায়। এটি পর্ব ৪২-এর reduction tree-রই বড় রূপ।
নিজের হাতে — POSIX thread দিয়ে সমান্তরাল যোগফল
নিচের C প্রোগ্রাম একই হিসাব (১২ কোটি সংখ্যার একটি যোগফল) ১, ২ আর ৪টি POSIX thread-এ ভাগ করে চালায় আর সময়, speedup আর efficiency ছাপে। প্রতিটি thread-এর ফল আলাদা ৬৪-byte struct-এ, যাতে দুটি thread একই cache line-এ না লেখে (false sharing, পর্ব ৪৮)। প্রোগ্রামটি শুরুতে শুধু জানায় এই যন্ত্রে কয়টি logical CPU আছে:
#define _GNU_SOURCE
#include <stdio.h>
#include <pthread.h>
#include <time.h>
#include <unistd.h>
#define N 120000000UL /* total work, split among threads */
typedef struct { unsigned long lo, hi, sum; char pad[40]; } Job; /* 64 B each: no false sharing */
static void *work(void *p) {
Job *j = p; unsigned long s = 0; /* private accumulator, written once */
for (unsigned long i = j->lo; i < j->hi; i++) s += (i ^ (i >> 3)) & 0xFF;
j->sum = s; return NULL;
}
static double now(void) {
struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t);
return (double)t.tv_sec + (double)t.tv_nsec / 1e9;
}
int main(void) {
printf("logical CPUs online here: %ld\n", sysconf(_SC_NPROCESSORS_ONLN));
double t1 = 0;
for (int T = 1; T <= 4; T *= 2) {
pthread_t th[4]; Job job[4]; unsigned long total = 0;
double t0 = now();
for (int k = 0; k < T; k++) {
job[k].lo = N / (unsigned long)T * (unsigned long)k;
job[k].hi = (k == T - 1) ? N : N / (unsigned long)T * (unsigned long)(k + 1);
pthread_create(&th[k], NULL, work, &job[k]);
}
for (int k = 0; k < T; k++) { pthread_join(th[k], NULL); total += job[k].sum; }
double dt = now() - t0; if (T == 1) t1 = dt;
printf("threads=%d sum=%lu time=%.2f s speedup=%.2f efficiency=%.0f%%\n",
T, total, dt, t1 / dt, 100.0 * t1 / dt / T);
}
return 0;
}একটি run-এর আউটপুট (একটি shared virtual machine-এ; সময় প্রতি run-এ বদলায়, sum সবসময় একই):
logical CPUs online here: 2
threads=1 sum=15300000000 time=0.28 s speedup=1.00 efficiency=100%
threads=2 sum=15300000000 time=0.21 s speedup=1.30 efficiency=65%
threads=4 sum=15300000000 time=0.21 s speedup=1.36 efficiency=34%
প্রথমে সঠিকতা: তিনবারই sum=15300000000 — কাজ ভাগ করায় ফল বদলায়নি (integer যোগ, তাই ক্রম বদলালেও সমস্যা নেই)। এবার সৎ হিসাবটি: এই runner একটি shared virtual machine, যেখানে মাত্র ২টি logical CPU. ২ thread-এ speedup আমাদের কয়েকটি run-এ ০.৭ থেকে ১.৪-এর মধ্যে ঘুরেছে — একবার তো ২ thread এক thread-এর চেয়েও ধীর চলেছে, আর ২-এর কাছে একবারও আসেনি। সম্ভাব্য কারণ, দুটি logical CPU হয়তো একই physical core-এর দুটি SMT thread (পর্ব ৪৩), বা একই host-এ অন্য কাজও চলছে। ৪ thread-এ speedup আর বাড়েনি, efficiency নেমে ৩৪% — CPU-র চেয়ে বেশি thread দিলে OS সেগুলোকে পালা করে চালায়, বাড়তি কাজ হয় না। নিজের ৮-core ল্যাপটপে চালালে ৪ thread পর্যন্ত speedup সাধারণত ৪-এর অনেক কাছে আসবে, কারণ এই কাজে মেমরি প্রায় লাগে না আর thread-দের মধ্যে কোনো কথা নেই। এটি একটি “embarrassingly parallel” কাজ — বাস্তবের বেশিরভাগ কাজ এত সৌভাগ্যবান নয়।
৪ · UMA, NUMA আর ccNUMA — মেমরি কত দূরে
প্রথম দিকের shared memory যন্ত্রে সব প্রসেসর একটি bus দিয়ে একটি মেমরির সাথে যুক্ত ছিল; যেকোনো প্রসেসর থেকে যেকোনো ঠিকানায় যেতে একই সময় লাগত। একে বলে UMA (Uniform Memory Access), বা symmetric multiprocessor (SMP). সমস্যা — সবাই একটি bus আর একটি memory controller ভাগ করে, তাই প্রসেসর বাড়লে লাইন লম্বা হয়।
সমাধান হলো মেমরিকে ভাগ করে প্রতিটি প্রসেসর-গুচ্ছের (একে বলে NUMA node, সাধারণত একটি socket) পাশে রাখা। প্রতিটি node-এর নিজের memory controller আর DRAM; সব node মিলে তবু একটিই address space, শুধু দূরের node-এর মেমরিতে যেতে interconnect পেরোতে হয়। এটি NUMA (Non-Uniform Memory Access). x86 server-এ এটি মূলধারায় আসে ২০০০-এর দশকে, যখন memory controller CPU chip-এর ভেতরে ঢোকে (AMD Opteron, পরে Intel Nehalem). আজ দুই-socket server তো বটেই, অনেক বড় একক chip-এর ভেতরেও কয়েকটি NUMA অঞ্চল থাকে।
আপনার ডেস্কের ড্রয়ার আর পাশের বিভাগের আলমারি — দুটিই অফিসের, দুটি থেকেই ফাইল আনা যায়। কিন্তু ড্রয়ার খুলতে এক সেকেন্ড, পাশের বিভাগে হেঁটে যেতে এক মিনিট। NUMA-তে সব মেমরি “আপনার”, কিন্তু সব সমান কাছে নয়।
কত ধীর? সংখ্যা প্রজন্ম আর সংযোগভেদে অনেক আলাদা, তাই মোটামুটি ধারণা হিসেবে নিন: একটি দুই-socket server-এ অন্য socket-এর DRAM-এ যাওয়া প্রায় দেড় থেকে দুই গুণ বেশি সময় নিতে পারে, আর পাওয়া bandwidth-ও কমে, কারণ socket-দের সংযোগটি মেমরি-বাসের চেয়ে সরু। Linux-এ numactl --hardware firmware-এর দেওয়া একটি “দূরত্বের টেবিল” দেখায়, যেখানে নিজের node-কে প্রথা অনুযায়ী 10 ধরা হয়:
available: 2 nodes (0-1)
node distances:
node 0 1
0: 10 21
1: 21 10
(উদাহরণ — একটি কাল্পনিক দুই-socket server-এর ছাঁচ; আপনার যন্ত্রের সংখ্যা আলাদা হবে।) 21 মানে মোটামুটি “নিজের চেয়ে দুই গুণের কাছাকাছি দূর” — এটি আপেক্ষিক ইঙ্গিত, nanosecond-এর মাপ নয়।
First-touch — page কোথায় বসবে
প্রোগ্রাম malloc করলে OS সাথে সাথে physical page দেয় না; প্রথম যেদিন কোনো thread সেই page-এ লেখে বা পড়ে (page fault), তখন দেয় (পর্ব ৫০)। Linux-এর ডিফল্ট নীতিতে page বসে যে CPU প্রথম ছুঁয়েছে, তার node-এ। এর নাম first-touch নীতি। ফাঁদটি সাধারণ: main thread একা পুরো array শূন্য দিয়ে ভরে (initialise), তারপর দুই socket-এর ১৬টি thread কাজ করে। সব page তখন main thread-এর node-এ — অন্য socket-এর আটটি thread-এর প্রতিটি load দূরের মেমরিতে যায়, আর একটি socket-এর memory controller-এ সবার ভিড়।
প্রতিকার: যে thread যে টুকরায় কাজ করবে, সেই thread-ই সেটি প্রথমে ছোঁবে (সমান্তরাল initialisation, একই ভাগে); thread-কে নির্দিষ্ট core-এ আটকে রাখা (pinning), যাতে OS তাকে অন্য socket-এ না সরায়; প্রয়োজনে numactl --interleave বা mbind দিয়ে page ছড়িয়ে দেওয়া। নতুন Linux kernel চলার সময় page-কে ব্যবহারকারী thread-এর কাছে সরাতেও পারে (automatic NUMA balancing), তবে তার নিজের খরচ আছে। OS-এর দিকের scheduling আর affinity নিয়ে বিস্তারিত আছে OS কোর্সের multiprocessor scheduling পর্বে।
আজকের প্রায় সব NUMA যন্ত্রে hardware cache coherence-ও রাখে — যেকোনো node-এর cache-এ রাখা যেকোনো ঠিকানার কপি মেলানো থাকে। একে বলে ccNUMA (cache-coherent NUMA). Coherence না থাকলে সফটওয়্যারকে নিজে cache flush করতে হতো, যা প্রায় অসম্ভব কঠিন। কিন্তু অনেক node-এ coherence কীভাবে টানা যায়? উত্তর — directory.
৫ · Directory-based coherence — গভীরে
পর্ব ৫৩-এ snooping protocol দেখেছেন: প্রতিটি miss সবাইকে broadcast করা হয়, আর সব cache “আড়ি পেতে” শোনে। চার-আট core-এ এটি সুন্দর কাজ করে। কিন্তু ৬৪ core-এ প্রতিটি miss-এ ৬৩টি cache-কে জিজ্ঞাসা করা মানে network আর cache-এর tag-এ বিপুল বাড়তি ট্রাফিক, যার বেশিরভাগ উত্তর “আমার কাছে নেই”। Directory-based coherence broadcast-এর বদলে একটি খাতা রাখে: প্রতিটি মেমরি block-এর জন্য — এটি এখন কোন কোন cache-এ আছে, আর কারো কাছে পরিবর্তিত (dirty) কপি আছে কি না।
লাইব্রেরির ক্যাটালগ ভাবুন। কোনো বই কার কাছে, তা জানতে পুরো শহরে মাইকিং করা (snooping) যায়, বা লাইব্রেরির খাতা দেখা যায় — “বইটি রহিম আর করিমের কাছে, কপি আছে”। খাতা দেখলে শুধু সংশ্লিষ্ট দুজনকেই খবর দিতে হয়।
প্রতিটি block-এর একটি home node থাকে — যে node-এর মেমরিতে ঠিকানাটি পড়ে, সাধারণত ঠিকানার কিছু bit দেখে ঠিক হয়। Block-এর directory entry থাকে সেই home-এ। সবচেয়ে সরল রূপ full bit-vector directory: প্রতিটি block-এর জন্য N core-এর জন্য N টি presence bit (কোন cache-এ কপি আছে) আর একটি-দুটি state bit.
খাতার দাম — storage overhead
Block-এর আকার B byte = ৮B bit, আর entry-তে N + ১ bit (N presence + ১ dirty) হলে overhead = (N + ১) / ৮B. ৬৪-byte line (৫১২ bit) ধরে:
| Core (N) | Entry (N + ১ bit) | Overhead = (N + ১) / ৫১২ |
|---|---|---|
| ১৬ | ১৭ bit | ৩.৩২% |
| ৬৪ | ৬৫ bit | ১২.৭০% |
| ২৫৬ | ২৫৭ bit | ৫০.২০% |
| ১০২৪ | ১০২৫ bit | ২০০.২০% — ডেটার দ্বিগুণ! |
অর্থাৎ overhead N-এর সাথে রৈখিকভাবে বাড়ে। একটি ৬৪-core যন্ত্রে ১৬ GiB মেমরি হলে block = ২৩৪ / ২৬ = ২২৮টি, directory = ২২৮ × ৬৫ bit ≈ ২.০৩ GiB — শুধু খাতার জন্য। তাই বাস্তব নকশা এর চেয়ে চালাক: limited pointer (কয়েকটি core-এর নম্বর রাখা, বেশি হলে broadcast), coarse vector (একটি bit একদল core-এর জন্য), আর sparse directory — সব মেমরির জন্য নয়, শুধু এখন কোনো cache-এ আছে এমন block-এর জন্য একটি cache-এর মতো টেবিল। আধুনিক multicore chip-এর shared L3-এর পাশে থাকা “snoop filter” মূলত এই শেষ ধারণা।
৩-hop read miss — dirty block পড়া
সবচেয়ে কঠিন ঘটনাটি ধাপে ধাপে দেখুন: P0 এমন একটি block X পড়তে চায়, যার home P2, আর যার পরিবর্তিত (Modified) কপি এখন P3-এর cache-এ। Presence bit লেখা হয়েছে P0 P1 P2 P3 ক্রমে:
| এই ধাপে | |
| Network-এ বার্তা | |
| Directory state | |
| Presence (P0 P1 P2 P3) | |
| P2-র মেমরির কপি | |
| P0-র cache | |
| P3-র cache |
- শুরু। P0-র load X-এ miss. Directory বলছে X dirty (M), presence-এ শুধু P3-এর bit জ্বলছে — মানে P2-র মেমরিতে X-এর কপি পুরোনো, আসল মান আছে P3-এর cache-এ।
- Hop ১। ঠিকানার bit দেখে P0 জানে X-এর home P2; সে network দিয়ে P2-কে “Read X” অনুরোধ পাঠায়। Snooping-এর মতো সবাইকে নয় — শুধু একজনকে।
- Home খাতা দেখে। P2 X-এর entry পড়ে: state M, মালিক P3. মেমরির কপি পুরোনো, তাই নিজে উত্তর দিতে পারে না। Entry-কে “অপেক্ষমাণ” (transient) অবস্থায় রাখে, যাতে এই সময়ে X নিয়ে অন্য অনুরোধ এলে সেগুলো অপেক্ষা করে।
- Hop ২। P2 অনুরোধটি মালিক P3-এর কাছে পাঠায় (forward বা intervention): “X-এর ডেটা সরাসরি P0-কে দিন।”
- Hop ৩। P3 ডেটা সরাসরি P0-কে পাঠায় আর নিজের কপি M থেকে S করে। সাথে একটি কপি home P2-কেও পাঠায়, যাতে মেমরি হালনাগাদ হয় — কিন্তু সেই বার্তা P0-র অপেক্ষার পথে নেই।
- খাতা হালনাগাদ। P2 entry লেখে: state S, presence 1 0 0 1 (P0 আর P3), মেমরি এখন নতুন। P0-র cache-এ X Shared অবস্থায়।
- তুলনা। P0-র অপেক্ষার পথে ৩টি বার্তা: P0→P2→P3→P0. সরল নকশায় ডেটা home ঘুরে আসত (P3→P2→P0) — ৪টি বার্তা। আর block clean হলে মাত্র ২টি (অনুরোধ আর উত্তর)। এজন্যই dirty-block miss সবচেয়ে দামি, আর “কম hop” নকশা পরিশ্রমের যোগ্য।
লেখার ক্ষেত্রেও একই খাতা কাজে লাগে: P0 X-এ লিখতে চাইলে home presence vector দেখে শুধু যাদের কপি আছে তাদের invalidate বার্তা পাঠায় আর তাদের ack জড়ো করে। ১০২৪ core-এ ২টি কপি থাকলে বার্তা ২টি — broadcast-এ হতো ১০২৩টি। এটাই directory-র স্কেল করার রহস্য।
৬ · Interconnection network — bus থেকে fat tree
Core, cache, memory controller বা পুরো কম্পিউটার — যাদের জুড়তে হবে তাদের বলি node, আর জোড়ার নকশাকে interconnection network. মহল্লার গলির নকশার মতো: কোন বাড়ি থেকে কোন বাড়িতে কত মোড় পেরোতে হয়, আর একসাথে কতগুলো রিকশা চলতে পারে — এই দুই প্রশ্নই আসল।
চারটি মাপকাঠি
- Degree — একটি node থেকে কয়টি link বের হয়। বেশি মানে বেশি port, বেশি তার, বেশি খরচ।
- Diameter — সবচেয়ে দূরের দুটি node-এর মধ্যে ন্যূনতম hop-সংখ্যা; সবচেয়ে খারাপ latency-র ইঙ্গিত। গড় দূরত্ব (average hops) সাধারণ latency বলে।
- Link-সংখ্যা — মোট তার; খরচের মোটা মাপ।
- Bisection bandwidth — node-গুলোকে সমান দুই ভাগ করতে সবচেয়ে কম যতগুলো link কাটতে হয় (সবচেয়ে খারাপ ভাগে), প্রতিটি link-এর bandwidth দিয়ে গুণ। অর্ধেক node যখন বাকি অর্ধেকের সাথে কথা বলে (যেমন বড় matrix transpose বা FFT), তখন এটিই বাধা।
টপোলজিগুলো
- Bus — একটি ভাগাভাগি করা তার; এক সময়ে একজনই কথা বলতে পারে। Broadcast বিনামূল্যে (snooping-এর জন্য আদর্শ), কিন্তু bandwidth node বাড়লেও বাড়ে না।
- Crossbar — N × N switch-এর জাল; যেকোনো node যেকোনো মুক্ত node-এর সাথে একসাথে কথা বলতে পারে। সেরা পারফরম্যান্স, কিন্তু N2টি crosspoint — বড় N-এ খরচ অসম্ভব।
- Ring — প্রতিটি node দুই পাশের প্রতিবেশীর সাথে। সস্তা আর সহজ, কিন্তু diameter N/2.
- 2D mesh — √N × √N ছক, প্রতিটি node উত্তর-দক্ষিণ-পূর্ব-পশ্চিমে জোড়া; ছাপা chip-এর সমতলে সুন্দর বসে।
- 2D torus — mesh-এর কিনারা ঘুরিয়ে উল্টো কিনারায় জোড়া (প্রতিটি সারি আর কলাম একটি ring). Diameter প্রায় অর্ধেক, bisection দ্বিগুণ।
- Hypercube — N = ২n node, প্রতিটির n-bit নম্বর; ঠিক এক bit-এ আলাদা দুটি node জোড়া। Diameter আর degree দুটিই log₂N.
- Fat tree — node-গুলো একটি গাছের পাতায়, switch-গুলো ডালে; ওপরের দিকে link “মোটা” (বেশি link বা বেশি bandwidth), যাতে গোড়ার কাছে ভিড় না হয়। পূর্ণ রূপে bisection N/2, আর যেকোনো দুই node-এর মধ্যে ওপরে উঠে নেমে আসার একাধিক পথ। আজকের data center-এর নেটওয়ার্ক মূলত এর আত্মীয় (Clos network).
নিচের simulator প্রতিটি টপোলজিকে একটি graph হিসেবে বানায়, প্রতিটি node থেকে BFS চালিয়ে diameter আর গড় hop গোনে, link গোনে, আর bisection সূত্র থেকে বসায় (bus আর crossbar-এ যেকোনো দুই node এক “hop”-এ ধরা হয়েছে):
// Interconnect metrics: build each topology as a graph, BFS for hop distances
function build(kind, N) {
const adj = Array.from({ length: N }, () => []), k = Math.round(Math.sqrt(N));
const link = (a, b) => { if (a !== b && !adj[a].includes(b)) { adj[a].push(b); adj[b].push(a); } };
for (let v = 0; v < N; v++) {
const r = Math.floor(v / k), c = v % k;
if (kind === "ring") link(v, (v + 1) % N);
if (kind === "mesh") { if (c + 1 < k) link(v, v + 1); if (r + 1 < k) link(v, v + k); }
if (kind === "torus") { link(v, r * k + (c + 1) % k); link(v, ((r + 1) % k) * k + c); }
if (kind === "hypercube") for (let b = 1; b < N; b <<= 1) link(v, v ^ b);
}
return adj; }
function hops(adj) { // diameter and average over all ordered pairs
let max = 0, sum = 0, N = adj.length;
for (let s = 0; s < N; s++) {
const d = new Array(N).fill(-1), q = [s]; d[s] = 0;
while (q.length) { const u = q.shift(); for (const w of adj[u]) if (d[w] < 0) { d[w] = d[u] + 1; q.push(w); } }
for (const x of d) { max = Math.max(max, x); sum += x; }
}
return [max, sum / (N * (N - 1))]; }
const bisect = { bus: N => 1, crossbar: N => N / 2, ring: N => 2, // formulas, in links
mesh: N => Math.sqrt(N), torus: N => 2 * Math.sqrt(N), hypercube: N => N / 2 };
const row = a => a.map((x, i) => String(x)[i ? "padStart" : "padEnd"]([11, 7, 10, 9, 9, 10][i])).join("");
for (const N of [16, 64]) {
console.log("N = " + N); console.log(row(["topology", "degree", "links", "diameter", "avg-hops", "bisection"]));
for (const kind of ["bus", "crossbar", "ring", "mesh", "torus", "hypercube"]) {
let deg = 1, links = kind === "bus" ? "1 shared" : N * N + " xpts", dia = 1, avg = 1; // bus, crossbar: 1 hop
if (kind in { ring: 1, mesh: 1, torus: 1, hypercube: 1 }) {
const adj = build(kind, N); [dia, avg] = hops(adj);
deg = Math.max(...adj.map(a => a.length)); links = adj.reduce((t, a) => t + a.length, 0) / 2;
}
console.log(row([kind, deg, links, dia, avg.toFixed(2), bisect[kind](N)]));
}
}আউটপুট (নির্ধারিত — প্রতিবার একই):
N = 16
topology degree links diameter avg-hops bisection
bus 1 1 shared 1 1.00 1
crossbar 1 256 xpts 1 1.00 8
ring 2 16 8 4.27 2
mesh 4 24 6 2.67 4
torus 4 32 4 2.13 8
hypercube 4 32 4 2.13 8
N = 64
topology degree links diameter avg-hops bisection
bus 1 1 shared 1 1.00 1
crossbar 1 4096 xpts 1 1.00 32
ring 2 64 32 16.25 2
mesh 4 112 14 5.33 8
torus 4 128 8 4.06 16
hypercube 6 192 6 3.05 32
৬৪ node-এর সারিগুলো পাশাপাশি রাখুন। Ring-এ diameter ৩২ আর bisection মাত্র ২ — দুটি link কাটলেই যন্ত্র দুই টুকরা। ৮ × ৮ mesh-এ diameter ২(৮ − ১) = ১৪, bisection ৮; torus-এ wrap-around link-এর জন্য diameter ৮, bisection ১৬ — link মাত্র ১১২ থেকে ১২৮. ৬-মাত্রার hypercube-এ diameter ৬, bisection ৩২, কিন্তু degree ৬ আর link ১৯২ — node বাড়লে degree-ও বাড়ে, যা chip বা বোর্ডে তার টানার বড় সমস্যা। আরেকটি মজার মিল: ১৬ node-এ torus আর hypercube-এর প্রতিটি সংখ্যা হুবহু এক — কারণ ৪ × ৪ torus আর ৪-মাত্রার hypercube আসলে একই graph (৪-node-এর ring = দুই মাত্রার cube, তার দুটির গুণফল)। Crossbar-এর crosspoint ৬৪-তে ৪০৯৬ — এজন্যই crossbar ছোট switch-এর ভেতরে থাকে, পুরো যন্ত্র জুড়ে নয়।
| Topology (N node) | Degree | Diameter | Link | Bisection (link) |
|---|---|---|---|---|
| Bus | ১ | ১ | ১ (ভাগাভাগি) | ১ |
| Crossbar | ১ | ১ | N2 crosspoint | N/2 |
| Ring | ২ | ⌊N/2⌋ | N | ২ |
| 2D mesh (√N × √N) | ৪ | ২(√N − ১) | ২√N(√N − ১) | √N |
| 2D torus | ৪ | ২⌊√N/2⌋ | ২N | ২√N |
| Hypercube | log₂N | log₂N | (N/2)·log₂N | N/2 |
Hypercube-এ routing — bit ঠিক করে করে
Hypercube-এর সৌন্দর্য হলো routing-এর জন্য কোনো টেবিল লাগে না। উৎস আর গন্তব্যের নম্বর XOR করলে যে bit-গুলো ১, ঠিক সেই মাত্রাগুলো পেরোতে হবে; hop-সংখ্যা = XOR-এর ১-এর সংখ্যা। প্রতিটি switch একটি নির্দিষ্ট ক্রমে (নিচের bit থেকে ওপরে) প্রথম অমিল bit-টি উল্টে পরের node-এ পাঠায় — একে বলে dimension-order বা e-cube routing:
| এই ধাপে | |
| বর্তমান node | |
| বাকি অমিল (বর্তমান ⊕ গন্তব্য) | |
| এই মাত্রায় কাজ | |
| Hop (এ পর্যন্ত) |
- শুরু। Packet আছে node 0101-এ, যেতে হবে 1110-এ। প্রতিটি node-এর চারটি প্রতিবেশী — চারটি bit-এর যেকোনো একটি উল্টে।
- XOR. 0101 ⊕ 1110 = 1011. Bit 0, 1 আর 3 আলাদা, bit 2 মেলে। তাই ঠিক ৩টি hop লাগবে — এর চেয়ে কম সম্ভব নয়, কারণ প্রতিটি hop একটিই bit বদলায়।
- মাত্রা ০। সবচেয়ে নিচের অমিল bit 0; উল্টে 0101 → 0100. বাকি অমিল 0100 ⊕ 1110 = 1010.
- মাত্রা ১। Bit 1 উল্টে 0100 → 0110. বাকি অমিল 1000.
- মাত্রা ২। Bit 2 ইতিমধ্যে মেলে (দুটিতেই 1) — এই মাত্রায় কোনো hop নয়।
- মাত্রা ৩। Bit 3 উল্টে 0110 → 1110 — গন্তব্য। বাকি অমিল 0000.
- সারসংক্ষেপ। ৩ hop = XOR-এর ১-এর সংখ্যা। সবচেয়ে খারাপ ক্ষেত্রে (যেমন 0000 থেকে 1111) চারটি bit-ই আলাদা — diameter = log₂১৬ = ৪. সব packet একই ক্রমে মাত্রা পেরোয় বলে চক্রাকার অপেক্ষা তৈরি হয় না, তাই এই routing deadlock-মুক্ত; mesh-এর “আগে X, পরে Y” routing-ও একই নীতির।
৭ · Chip থেকে data center — on-chip network, cluster আর supercomputer
Chip-এর ভেতরের network
একই নীতি chip-এর ভেতরেও খাটে, শুধু মাপ millimeter-এ। কয়েকটি core-এ crossbar বা bus চলে; দশ-বিশটি core আর L3-এর টুকরা জুড়তে অনেক নকশা ring ব্যবহার করেছে (যেমন ২০১১-র দিকের Intel-এর desktop chip, প্রকাশিত বিবরণ অনুযায়ী); আরও বেশি core-এ 2D mesh (যেমন Intel-এর পরের প্রজন্মের server chip). এদের বলে network-on-chip (NoC): প্রতিটি tile-এ একটি ছোট router, packet-এর মতো বার্তা। AMD-র chiplet নকশায় আবার কয়েকটি আলাদা die একটি package-এর ভেতরে নিজস্ব সংযোগে জোড়া। ঠিক কোন chip-এ কোন টপোলজি, তা প্রজন্মভেদে বদলায় — নির্মাতার প্রকাশিত নথি দেখে নিশ্চিত হবেন।
Cluster আর warehouse-scale computer
একাধিক সাধারণ server-কে একটি দ্রুত network দিয়ে জুড়ে একটি যন্ত্রের মতো ব্যবহার করলে তা cluster. প্রতিটি server-এর ভেতরে shared memory, বাইরে message passing. এর চরম রূপ হলো warehouse-scale computer (WSC) — একটি বিশাল ভবনে হাজার হাজার server, যারা মিলে একটি সেবা চালায় (সার্চ, ভিডিও, ইমেইল, AI). এখানে নকশার মাপকাঠি বদলে যায়: প্রতি টাকায় আর প্রতি watt-এ কত কাজ, আর অংশ নষ্ট হলেও সেবা চালু রাখা — এত যন্ত্রে প্রতিদিনই কোনো না কোনো disk বা server বিকল হয়, তাই সফটওয়্যারকে ধরে নিতে হয় যে hardware ভাঙবে। এদের নেটওয়ার্ক সাধারণত fat tree বা Clos ধাঁচের, সস্তা সাধারণ switch দিয়ে (বিস্তারিত: Computer Networks কোর্সের data center network পর্ব)।
Supercomputer আর TOP500
Supercomputer মূলত বিশেষভাবে টিউন করা cluster: খুব দ্রুত আর কম latency-র interconnect (torus, dragonfly, fat tree), প্রায়ই প্রতি node-এ GPU, আর বৈজ্ঞানিক হিসাবের জন্য সাজানো সফটওয়্যার। ১৯৯৩ সাল থেকে বছরে দুবার TOP500 তালিকা প্রকাশিত হয়, যা যন্ত্রগুলোকে সাজায় একটি নির্দিষ্ট benchmark — LINPACK (HPL), একটি বড় dense linear equation system সমাধান — এ পাওয়া FLOP/s দিয়ে। ২০২২ সালে প্রথমবার একটি যন্ত্র এই তালিকায় HPL-এ ১ exaFLOP/s (১০১৮) ছাড়ায় বলে ঘোষণা করা হয়। তালিকা প্রতি ছয় মাসে বদলায়, তাই এখন কে শীর্ষে তা তালিকার ওয়েবসাইটে দেখে নিন। মনে রাখবেন HPL প্রায় পুরোটাই dense matrix গুণ — অংশ ৮-এর ভাষায় খুব উঁচু operational intensity — তাই বাস্তবের অনেক প্রোগ্রাম একই যন্ত্রে peak-এর অনেক কম পায়।
শক্তির চোখে
বড় যন্ত্রে বিদ্যুৎই প্রথম সীমা: একটি বড় supercomputer বা data center কয়েক থেকে কয়েক দশ megawatt পর্যন্ত টানতে পারে, যা একটি ছোট শহরের চাহিদার কাছাকাছি। তাই TOP500-এর পাশে Green500 তালিকা যন্ত্রগুলোকে সাজায় প্রতি watt-এ FLOP/s দিয়ে, আর data center মাপা হয় PUE দিয়ে — মোট বিদ্যুৎ ÷ শুধু কম্পিউটারের বিদ্যুৎ (বাকিটা ঠান্ডা রাখা আর বিদ্যুৎ রূপান্তরে)। Chip-এর ভেতরেও একই সত্য: একটি সংখ্যা DRAM থেকে আনতে যে energy লাগে, তা একটি floating point গুণের চেয়ে বহু গুণ বেশি — বিভিন্ন প্রকাশিত হিসাবে শতগুণের কাছাকাছি বা তারও বেশি। সমান্তরাল নকশায় তাই হিসাব সস্তা, ডেটা সরানো দামি; বিস্তারিত পর্ব ৫৬-এ।
৮ · Hardware-এর চোখে সমান্তরালতার বাধা — আর roofline model
Amdahl-এর সূত্রের বাইরেও তিনটি বাধা বাস্তব speedup খেয়ে ফেলে:
- Load imbalance — কাজ সমান ভাগ হয়নি। ১৬টি thread-এর ১৫টি ১ সেকেন্ডে শেষ, একটি ২ সেকেন্ডে — সবাইকে ২ সেকেন্ড অপেক্ষা করতে হয়, efficiency প্রায় অর্ধেক। প্রতিকার: ছোট টুকরায় ভাগ করে যে আগে শেষ করে সে নতুন টুকরা নেয় (dynamic scheduling, work stealing).
- যোগাযোগ (communication) — ডেটা এক core থেকে আরেক core-এ বা node-এ নেওয়া। Shared memory-তে এটি লুকানো থাকে coherence miss আর দূরের NUMA মেমরির মধ্যে; message passing-এ প্রতিটি বার্তার একটি স্থির খরচ (latency) আর আকার ÷ bandwidth. টুকরা ছোট করলে load balance ভালো হয়, কিন্তু যোগাযোগের অনুপাত বাড়ে — এই টানাপোড়েন সমান্তরাল নকশার কেন্দ্রে।
- Synchronisation-এর দাম — barrier-এ সবার অপেক্ষা, lock-এর জন্য লাইন, atomic instruction-এ একটি cache line-এর core থেকে core-এ লাফালাফি। একটি জনপ্রিয় counter-এ ৬৪টি core একসাথে
atomic_addকরলে প্রতিবার line-টিকে নতুন মালিকের কাছে যেতে হয় — কাজটি কার্যত serial হয়ে যায় (software-এর দিক: OS কোর্সের atomics আর spinlock পর্ব)।
Roofline model — একটি ছবিতে সীমা
একটি kernel (প্রোগ্রামের ভারী লুপ) দ্রুত চলবে কি না, তা দুটি ছাদের নিচের একটি ঠিক করে: যন্ত্রের সর্বোচ্চ হিসাবের গতি (peak FLOP/s), আর মেমরি থেকে ডেটা আনার গতি (bandwidth, GB/s). এদের যোগসূত্র হলো kernel-এর operational intensity (I) = মোট floating point কাজ ÷ DRAM-এর সাথে আনা-নেওয়া byte. ২০০৯ সালে Samuel Williams, Andrew Waterman আর David Patterson এটিকে একটি সরল ছবিতে আনেন — roofline model:
Attainable GFLOP/s = min(Peak GFLOP/s, Bandwidth × I)দুই ছাদ যেখানে মেলে — I = Peak ÷ Bandwidth — তাকে বলে ridge point. এর বাঁয়ে kernel memory-bound (core অলস বসে ডেটার অপেক্ষায়), ডানে compute-bound. একটি kernel কোথায় বসে, ধাপে ধাপে দেখুন। যন্ত্রের সংখ্যাগুলো বোঝার জন্য ধরা অনুমান, কোনো নির্দিষ্ট chip-এর নয়:
| এই ধাপে | |
| হিসাব | |
| Memory ছাদ (BW × I) | |
| Attainable | |
| রায় |
- দুই ছাদ। অনুমিত যন্ত্রটি প্রতি সেকেন্ডে ১০০০ GFLOP হিসাব করতে পারে, কিন্তু DRAM থেকে মাত্র ১০০ GB আনতে পারে। এগুলো ব্যাখ্যার জন্য ধরা গোল সংখ্যা।
- Ridge point. ১০০০ ÷ ১০০ = ১০ FLOP/byte. প্রতি byte-এ অন্তত ১০টি হিসাব না করলে core-কে কখনো পুরো ব্যস্ত রাখা যাবে না।
- FLOP গোনা। প্রতিটি output বিন্দুতে পাঁচটি মানের যোগ (৪টি যোগ) আর ০.২ দিয়ে একটি গুণ — মোট ৫ FLOP.
- Byte গোনা। প্রতিবেশীরা cache-এ থেকে আবার ব্যবহার হয় ধরে নিলে প্রতিটি input মান DRAM থেকে একবারই আসে (৪ byte), আর output একবার লেখা হয় (৪ byte) — ৮ byte. (Cache-এ reuse না হলে byte অনেক বেশি, I আরও কম।)
- কোথায় বসে। I = ৫ ÷ ৮ = ০.৬২৫, ridge-এর (১০) অনেক বাঁয়ে। Memory ছাদ ১০০ × ০.৬২৫ = ৬২.৫ GFLOP/s, যা peak-এর মাত্র ৬.২৫%. ৯৩%-এর বেশি হিসাবের ক্ষমতা অলস।
- কী করলে বাড়ে। Core দ্বিগুণ করলে ফল একই ৬২.৫ — ছাদটি হিসাবের নয়। Bandwidth দ্বিগুণে ১২৫. আর কয়েকটি সময়-ধাপ cache-এ থাকা অবস্থায় একসাথে চালালে (temporal blocking) একই byte-এ ৪ গুণ হিসাব, I = ২.৫, attainable ২৫০. Roofline-এর মূল শিক্ষা: memory-bound kernel-এ আগে I বাড়ান।
এবার কয়েকটি পরিচিত kernel একসাথে। ঘন matrix গুণে (n × n) কাজ ২n3 FLOP, কিন্তু আদর্শভাবে প্রতিটি matrix একবারই আনা-নেওয়া হয় (১২n2 byte), তাই I = n/৬ — n বড় হলে বিশাল। কিন্তু সাধারণ তিন-লুপের রূপে ভেতরের লুপ প্রতি ২ FLOP-এ দুটি নতুন সংখ্যা আনে (I = ০.২৫); b × b tile-এ ভাগ করলে (পর্ব ৪৮-এর blocking) দুটি tile আনায় (৮b2 byte) ২b3 FLOP হয়, I = b/৪:
// Roofline: attainable GFLOP/s = min(peak, bandwidth x operational intensity)
const PEAK = 1000, BW = 100; // ASSUMED machine: 1000 GFLOP/s (fp32), 100 GB/s DRAM
const ridge = PEAK / BW;
console.log("assumed peak " + PEAK + " GFLOP/s, bandwidth " + BW + " GB/s, ridge point = " + ridge + " FLOP/byte\n");
// operational intensity = FLOPs / bytes moved to and from DRAM (fp32 = 4 bytes)
const kernels = [
["saxpy y=a*x+y", 2, 3 * 4], // 1 mul + 1 add; read x, read y, write y
["dot product", 2, 2 * 4], // 1 mul + 1 add; read x, read y
["5-point stencil", 5, 2 * 4], // 4 adds + 1 mul; read in once (cache reuse), write out
["matmul naive", 2, 2 * 4], // inner loop: a[i][k]*b[k][j], both from DRAM
["matmul tile b=32", 2 * 32 ** 3, 2 * 32 * 32 * 4], // two 32x32 tiles in, 2*b^3 FLOPs
["matmul n=1024 ideal", 2 * 1024 ** 3, 3 * 1024 * 1024 * 4], // each matrix moved once
];
console.log("kernel".padEnd(21) + "FLOP/byte".padStart(10) + "attainable".padStart(12) + " % of peak bound");
for (const [name, flops, bytes] of kernels) {
const oi = flops / bytes, att = Math.min(PEAK, BW * oi);
console.log(name.padEnd(21) + oi.toFixed(3).padStart(10) + att.toFixed(1).padStart(12) +
(100 * att / PEAK).toFixed(1).padStart(10) + "% " + (oi < ridge ? "memory" : "compute"));
}
// what if we double the cores (peak) or the bandwidth?
const att = (oi, p, b) => Math.min(p, b * oi);
console.log("\nstencil (0.625): 2x peak -> " + att(0.625, 2 * PEAK, BW) + ", 2x bandwidth -> " + att(0.625, PEAK, 2 * BW));
console.log("tile b=32 (8.0): 2x peak -> " + att(8, 2 * PEAK, BW) + ", 2x bandwidth -> " + att(8, PEAK, 2 * BW));আউটপুট (নির্ধারিত — প্রতিবার একই):
assumed peak 1000 GFLOP/s, bandwidth 100 GB/s, ridge point = 10 FLOP/byte
kernel FLOP/byte attainable % of peak bound
saxpy y=a*x+y 0.167 16.7 1.7% memory
dot product 0.250 25.0 2.5% memory
5-point stencil 0.625 62.5 6.3% memory
matmul naive 0.250 25.0 2.5% memory
matmul tile b=32 8.000 800.0 80.0% memory
matmul n=1024 ideal 170.667 1000.0 100.0% compute
stencil (0.625): 2x peak -> 62.5, 2x bandwidth -> 125
tile b=32 (8.0): 2x peak -> 800, 2x bandwidth -> 1000
saxpy (y = a·x + y): প্রতি উপাদানে ২ FLOP কিন্তু ১২ byte (x পড়া, y পড়া, y লেখা), I = ০.১৬৭ — peak-এর মাত্র ১.৭%. Dot product আর সাধারণ matmul-এর ভেতরের লুপ দুটিই ০.২৫. লক্ষ করুন, একই matrix গুণ লেখার ধরন বদলালেই ২৫ থেকে ৮০০ GFLOP/s — ৩২ গুণ! b = ৩২-এর tile-এ I = ৮, ridge-এর (১০) একটু বাঁয়ে, তাই তখনো সামান্য memory-bound; bandwidth দ্বিগুণ করলে ১০০০ ছুঁয়ে ফেলে। b ≥ ৪০ হলে (b/৪ ≥ ১০) tile-matmul compute-bound. পুরো ১০২৪ × ১০২৪ গুণ আদর্শ reuse-এ I ≈ ১৭০.৭ — ছাদের একেবারে ডানে। এজন্যই HPL-এ যন্ত্র peak-এর কাছে পৌঁছায়, আর stencil বা sparse কাজে অনেক দূরে থাকে।
আরও গভীরে: আসল roofline-এ একাধিক ছাদ আঁকা হয় — L1, L2, L3 আর DRAM-এর আলাদা bandwidth রেখা, আর SIMD বা FMA ছাড়া কম peak-এর আলাদা অনুভূমিক রেখা; একটি kernel কোন রেখার নিচে আটকে আছে, তা দেখে বোঝা যায় কোন optimisation আগে করতে হবে। Intel Advisor বা NVIDIA Nsight Compute-এর মতো টুল hardware counter মেপে আসল kernel-কে এই ছবিতে বসিয়ে দেখায়। Directory-র গবেষণায় আবার প্রশ্ন হলো কত ছোট “sparse directory” রাখলে চলে, আর directory-র জায়গা খালি করতে কোনো block বাদ দিলে সব cache থেকে সেটির কপি মুছতে হয় — এই “directory-induced invalidation” বড় chip-এ লুকানো খরচ।
৯ · Heterogeneous system — big.LITTLE, P-core আর E-core, accelerator
এতক্ষণ ধরে নিয়েছি সব core একই রকম। আজকের ফোন আর ল্যাপটপের chip তা নয়। ২০১১ সালে ARM ঘোষণা করে big.LITTLE: একই chip-এ কয়েকটি বড়, দ্রুত, বেশি power-খাওয়া core আর কয়েকটি ছোট, ধীর, সাশ্রয়ী core — একই ISA-তে, তাই একই প্রোগ্রাম যেকোনোটিতে চলে। WhatsApp-এ বার্তা আসার খবর ছোট core-এ, গেম বা ভিডিও এডিট বড় core-এ। পরে Apple-এর ল্যাপটপ chip আর ২০২১ সালে Intel-এর desktop chip-ও একই ধারণা আনে — Intel এদের বলে P-core (performance) আর E-core (efficiency); chip OS-কে কোন thread কোথায় ভালো চলবে সে বিষয়ে ইঙ্গিত দেয়, যদিও শেষ সিদ্ধান্ত OS scheduler-এর।
যুক্তিটি power wall থেকেই আসে। একটি বড় out-of-order core-এর পারফরম্যান্স আকারের সাথে মোটামুটি বর্গমূলের মতো ধীরে বাড়ে, কিন্তু power প্রায় আকারের সমানুপাতে — তাই হালকা কাজে ছোট core প্রতি joule-এ অনেক বেশি কাজ দেয়। এর পরের ধাপ হলো সম্পূর্ণ আলাদা ধরনের core — GPU, AI-এর NPU, ভিডিও আর ছবির encoder — যা নির্দিষ্ট কাজে CPU-র চেয়ে বহু গুণ দক্ষ। এমন মিশ্র chip-কে বলে heterogeneous system; accelerator নিয়ে পুরো আলোচনা পর্ব ৫৫-এ। প্রোগ্রামারের জন্য এর অর্থ: “কয়টি core আছে” প্রশ্নের উত্তর আর একটি সংখ্যা নয় — কোন ধরনের core, কোন কাজের জন্য।
পরীক্ষা ও ইন্টারভিউতে — কয়েকটি প্রচলিত ধরনের প্রশ্ন আর এক লাইনের উত্তর:
- Amdahl আর Gustafson-এর সূত্রের পার্থক্য কী? Amdahl স্থির কাজে (strong scaling) speedup = ১/(s + (১−s)/N), সীমা ১/s; Gustafson কাজ N-এর সাথে বাড়ায় (weak scaling), speedup = s + (১−s)N.
- ৬৪ node-এর hypercube-এর diameter আর bisection কত? log₂৬৪ = ৬ hop; bisection N/2 = ৩২টি link.
- UMA আর NUMA-র পার্থক্য কী? UMA-তে সব মেমরি সব প্রসেসর থেকে সমান দূরে; NUMA-তে প্রতিটি node-এর নিজের কাছের মেমরি দ্রুত, অন্যের ধীর, তবে address space একটিই।
- Snooping কেন অনেক core-এ স্কেল করে না, directory কীভাবে সাহায্য করে? প্রতিটি miss broadcast হয়; directory শুধু কপি-ধারীদের বার্তা পাঠায়।
- Roofline-এ ridge point কী? Peak FLOP/s ÷ memory bandwidth — এর চেয়ে কম operational intensity-র kernel memory-bound.
১০ · AI-এর যুগে — সমান্তরালতার হিসাব AI দিয়ে, যাচাই নিজে
“এই কোড ৬৪ core-এ কত দ্রুত হবে?” বা “এই server-এর NUMA latency কত?” — এমন প্রশ্নে AI আত্মবিশ্বাসী সংখ্যা দেয়। এই বিষয়ে তার সাধারণ ভুলগুলো:
- Amdahl আর Gustafson-এর s গুলিয়ে ফেলা, বা strong scaling-এর প্রশ্নে Gustafson বসিয়ে অবাস্তব রৈখিক speedup দেখানো।
- Topology-র সূত্রে ভুল — mesh আর torus-এর diameter বা bisection অদলবদল, hypercube-এর link-সংখ্যায় ২ দিয়ে ভাগ ভুলে যাওয়া (N·log₂N বনাম (N/2)·log₂N).
- Directory overhead-এ bit আর byte গুলিয়ে ফেলা (৬৪ bit ÷ ৬৪ byte-কে ১০০% বলা), আর state bit বাদ দেওয়া।
- Roofline-এ byte গোনায় ভুল — write-এর byte বাদ, বা cache reuse ধরে নেওয়া/না-নেওয়া স্পষ্ট না করা; peak আর bandwidth-এর একক মেলানো (GB/s বনাম GiB/s, fp32 বনাম fp64) ভুলে যাওয়া।
- নির্দিষ্ট chip-এর core-সংখ্যা, NUMA latency, interconnect-এর টপোলজি বা TOP500-এর বর্তমান ক্রম বানিয়ে বলা — এগুলো বদলায়, আর AI-এর জ্ঞান পুরোনো হতে পারে।
AI-কে হিসাবের পরীক্ষক হিসেবে ব্যবহার করুন — প্রতিটি সংখ্যার উৎস আর সূত্র চেয়ে নিন:
AI-এর দেওয়া সমান্তরাল-হিসাব যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Amdahl ১৯৬৭ সালে সমান্তরাল যন্ত্র নিয়ে সন্দেহ প্রকাশ করেছিলেন, Gustafson ১৯৮৮ সালে উল্টো যুক্তি দেন। দুজনের কে ঠিক — আর আজকের বাস্তবতা কী বলে?
Gene Amdahl ছিলেন IBM-এর বড় mainframe-এর একজন প্রধান নকশাকার। ১৯৬৭ সালের একটি সম্মেলনে তিনি যুক্তি দেন যে অনেক সরল প্রসেসর জুড়ে বানানো যন্ত্রের চেয়ে একটি দ্রুত প্রসেসর বেশি কাজের, কারণ বাস্তব প্রোগ্রামের একটি উল্লেখযোগ্য অংশ — ডেটা গোছানো, শর্ত পরীক্ষা, I/O — ভাগ করা যায় না। তাঁর লেখায় সূত্রটি সরাসরি বীজগণিত আকারে ছিল না; পরে অন্যরা সেটিকে আজকের পরিচিত রূপে লেখেন। তবু বার্তাটি স্পষ্ট: serial অংশই ছাদ।
বিশ বছর পর Sandia National Laboratories-এ John Gustafson আর তাঁর সহকর্মীরা ১০২৪-প্রসেসরের একটি hypercube যন্ত্রে কয়েকটি বৈজ্ঞানিক প্রোগ্রামে প্রায় হাজার গুণের কাছাকাছি speedup দেখান — Amdahl-এর হিসাবে যা প্রায় অসম্ভব মনে হতো। এই কাজ প্রথম দিকের Gordon Bell Prize-এর একটি পায় বলে জানা যায়। Gustafson-এর ব্যাখ্যা: তাঁরা একই সমস্যা দ্রুত করেননি, বড় যন্ত্রে বড় সমস্যা চালিয়েছেন, আর তাতে serial অংশ প্রায় একই থেকেছে, সমান্তরাল অংশ বেড়েছে।
দুজনই ঠিক — ভিন্ন প্রশ্নে। একটি ফোনের অ্যাপ খুলতে কত সময় লাগে, একটি ওয়েব অনুরোধের latency, একটি compile — এগুলো স্থির আকারের কাজ, Amdahl-এর রাজ্য; তাই ফোনে ৮-১০টির বেশি CPU core দেওয়ার আগ্রহ কম, আর একটি core-এর গতি এখনো গুরুত্বপূর্ণ। আবহাওয়ার পূর্বাভাস, জলবায়ু মডেল, বড় AI model-এর training — এখানে বড় যন্ত্র পেলে মানুষ বড় সমস্যা চালায়, Gustafson-এর রাজ্য; তাই supercomputer লক্ষ লক্ষ core পর্যন্ত বাড়ে। বাস্তবে দুই সূত্রের কোনোটিই যোগাযোগ আর synchronisation-এর খরচ ধরে না, যা N-এর সাথে বাড়ে — তাই weak scaling-ও এক সময় হাঁপিয়ে ওঠে। আধুনিক পরিমাপে তাই দুই ধরনের scaling গ্রাফই প্রকাশ করা হয়, আর কোনটি মাপা হয়েছে তা স্পষ্ট লিখতে হয়।
মূল উপলব্ধি: “কত দ্রুত” প্রশ্নের আগে জিজ্ঞাসা করুন — কাজের আকার স্থির না বাড়ন্ত; উত্তরের অর্ধেক সেখানেই।
প্র ০২Shared memory প্রোগ্রাম লেখা সহজ, তবু হাজার node-এর যন্ত্রে message passing (MPI) কেন রাজত্ব করে? সফটওয়্যার দিয়ে cluster-এ shared memory বানানোর চেষ্টা কেন সফল হয়নি?
Shared memory-র সুবিধা হলো ডেটা ভাগ করতে কিছু লিখতে হয় না — pointer দিলেই হলো। কিন্তু এর দাম hardware দেয়: প্রতিটি store-এর খবর coherence protocol দিয়ে অন্য cache-এ পৌঁছাতে হয়, আর consistency-র নিয়ম রক্ষা করতে হয়। Node বাড়লে directory বড় হয়, দূরত্ব বাড়ে, আর একটি জনপ্রিয় cache line-এর জন্য লড়াই পুরো যন্ত্রকে ধীর করে। সবচেয়ে বড় সমস্যা হলো খরচটি অদৃশ্য — কোড দেখে বোঝা যায় না কোন load দূরের node থেকে আসবে।
Message passing উল্টো: প্রতিটি যোগাযোগ কোডে স্পষ্ট, তাই প্রোগ্রামার জানেন কোথায় খরচ হচ্ছে আর কীভাবে কমাতে হবে — বড় বার্তায় জড়ো করা, হিসাবের সাথে যোগাযোগ ঢেকে রাখা। Hardware-কে coherence রাখতে হয় না, তাই সাধারণ server আর দ্রুত network দিয়েই বিশাল যন্ত্র বানানো যায়। ১৯৮০-র দশকে প্রতিটি সমান্তরাল যন্ত্রের নির্মাতার নিজের message-passing library ছিল; ১৯৯৪ সালের দিকে বিভিন্ন প্রতিষ্ঠান মিলে MPI মান প্রকাশ করে, আর তারপর থেকে বৈজ্ঞানিক কোড এক যন্ত্র থেকে আরেক যন্ত্রে নেওয়া সহজ হয়। এই বহনযোগ্যতাই MPI-র টিকে থাকার বড় কারণ।
১৯৯০-এর দশকে গবেষকেরা চেষ্টা করেন সফটওয়্যার দিয়ে cluster-এ shared memory-র ভান তৈরি করতে (distributed shared memory) — virtual memory-র page fault ধরে দূরের node থেকে page আনা। ধারণা সুন্দর, কিন্তু একক হলো page (কয়েক KB), তাই দুই node একই page-এর আলাদা অংশে লিখলে পুরো page বারবার যাওয়া-আসা করে — false sharing-এর বিশাল রূপ। Network-এর latency microsecond-এর ঘরে, cache miss-এর হাজার গুণ। ফলে বেশিরভাগ প্রোগ্রাম আশানুরূপ দ্রুত হয়নি।
আজকের সমাধান মিশ্র: node-এর ভেতরে thread/OpenMP (বা GPU), বাইরে MPI; মাঝখানে PGAS ভাষা (যেমন একটি global array যার প্রতিটি অংশের “মালিক” স্পষ্ট) আর RDMA — network card সরাসরি দূরের মেমরিতে পড়ে-লেখে, CPU-কে না জাগিয়ে। অর্থাৎ shared address space-এর সুবিধা কিছুটা নেওয়া হচ্ছে, কিন্তু দূরত্বটি প্রোগ্রামারের চোখের সামনে রেখে।
মূল উপলব্ধি: বড় মাপে যোগাযোগের খরচ লুকিয়ে রাখা বিপজ্জনক; যে মডেল খরচকে দৃশ্যমান রাখে, সেটিই স্কেল করে।
প্র ০৩Bus-ভিত্তিক snooping থেকে directory-তে যাত্রার ইতিহাস কী? আজকের একটি বড় multicore chip-এর ভেতরে কি directory আছে?
১৯৮০-র দশকে প্রথম বাণিজ্যিক multiprocessor-গুলো (কয়েকটি থেকে কয়েক ডজন প্রসেসর) একটি shared bus-এ বসত, আর প্রতিটি cache bus-এ আড়ি পেতে coherence রাখত। নকশা সরল, কারণ bus-এ সবাই সবকিছু একই ক্রমে দেখে — এই “স্বাভাবিক ক্রম” consistency-কেও সহজ করত। কিন্তু প্রসেসর দ্রুত হওয়ার সাথে সাথে একটি bus-এর bandwidth আর বৈদ্যুতিক সীমা (লম্বা তার, অনেক লোড) কয়েক ডজন প্রসেসরের বেশি টানতে পারছিল না।
Directory-র ধারণা ১৯৭০-এর দশকের শেষেই প্রস্তাবিত হয়, কিন্তু বড় আকারে দেখান Stanford-এর DASH প্রকল্প (১৯৯০-এর দশকের শুরুতে) — কয়েকটি ছোট bus-ভিত্তিক cluster, তাদের মধ্যে directory দিয়ে coherence. এরপর SGI Origin 2000 (১৯৯৬-এর দিকে) directory-ভিত্তিক ccNUMA-কে বাণিজ্যিকভাবে শত প্রসেসর পর্যন্ত নিয়ে যায়। এই যন্ত্রগুলোই শিখিয়েছিল যে দূরের miss-এর latency কমানো (যেমন ৩-hop forwarding) আর directory-র মেমরি-খরচ নিয়ন্ত্রণ আসল চ্যালেঞ্জ।
আজ ইতিহাস chip-এর ভেতরে ঢুকে গেছে। একটি বড় server chip-এ কয়েক ডজন থেকে শতাধিক core, mesh বা অন্য NoC দিয়ে জোড়া — সেখানে broadcast অসম্ভব রকম দামি। তাই অনেক নকশায় shared L3-এর পাশে বা ভেতরে প্রতিটি line-এর জন্য “কোন core-এর private cache-এ কপি আছে” তার হিসাব থাকে; একসময় inclusive L3-এর প্রতিটি line-এ core-valid bit রাখার পদ্ধতি পরিচিত ছিল, পরে non-inclusive নকশায় আলাদা snoop filter. নাম “snoop filter” হলেও কাজটি sparse directory-র। বহু-socket যন্ত্রে socket-দের মধ্যেও একই ধরনের filter বা directory থাকে, যাতে প্রতিটি miss অন্য socket-এ broadcast না হয়।
তবে directory বিনামূল্যে নয়: এর নিজের জায়গা, entry বাদ দিলে কপি মুছে ফেলা, আর অনুরোধের জট সামলানোর জটিল অবস্থা-যন্ত্র (transient state). Protocol-এর ভুল খুঁজতে নির্মাতারা formal verification-এ বিপুল বিনিয়োগ করেন, কারণ একটি বিরল race-ও চুপচাপ ডেটা নষ্ট করতে পারে।
মূল উপলব্ধি: Coherence-এর মূল প্রশ্ন “কাকে জানাতে হবে?” — অল্প core-এ সবাইকে, অনেক core-এ খাতা দেখে শুধু যাদের দরকার।
প্র ০৪Supercomputer-এর ইতিহাসে hypercube, torus, fat tree, dragonfly — টপোলজির ফ্যাশন কেন বারবার বদলেছে? Data center কেন fat tree বা Clos বেছে নেয়?
১৯৮০-র দশকে hypercube ছিল তারকা। Caltech-এর Cosmic Cube আর তার পরের বাণিজ্যিক যন্ত্রগুলো (Intel-এর iPSC, nCUBE) hypercube-এ জোড়া ছিল, আর Connection Machine-এর chip-গুলোও hypercube-এ। কারণ: log₂N diameter, বড় bisection, আর XOR দিয়ে সরল routing. সমস্যা ধরা পড়ে মাপ বাড়লে — degree log₂N, তাই প্রতিটি node-এ অনেক port, আর হাজার node-এ তার টানা (বিশেষত দূরের মাত্রার লম্বা তার) কঠিন হয়ে ওঠে।
১৯৯০-এর দশকে তাই নিম্ন-মাত্রার torus জনপ্রিয় হয় — Cray-র T3D/T3E-তে 3D torus, পরে IBM-এর Blue Gene পরিবারে 3D থেকে 5D torus. প্রতিটি node-এর স্থির, অল্প কয়েকটি link, আর প্রতিবেশীর সাথে ছোট তার — বিজ্ঞানের বেশিরভাগ হিসাব (stencil, পদার্থের গ্রিড) প্রতিবেশীর সাথেই কথা বলে, তাই এটি ভালো মেলে। দুর্বলতা: diameter বড়, আর “সবাই সবার সাথে” ধরনের যোগাযোগে bisection কম পড়ে।
Fat tree-র ধারণা Charles Leiserson ১৯৮৫ সালে দেন, আর Thinking Machines-এর CM-5 এটি ব্যবহার করে। Data center-এ এর পুনর্জন্ম হয় ২০০৮-এর দিকের গবেষণায়: হাজার হাজার সস্তা, একই রকম switch দিয়ে বানানো Clos/fat tree পূর্ণ bisection দেয়, অনেক বিকল্প পথ দেয় (কয়েকটি switch নষ্ট হলেও চলে), আর একটি বিশাল দামি core router-এর ওপর নির্ভরতা কমায়। Data center-এর ট্রাফিক অনুমান করা যায় না — যেকোনো server যেকোনো server-এর সাথে কথা বলে — তাই bisection-ই রাজা।
২০০৮-এর দিকে প্রস্তাবিত dragonfly আরেক দিক নেয়: router-গুলো দলে, দলের ভেতরে প্রায় সবাই সবার সাথে, আর দলগুলোর মধ্যে অল্প কিন্তু লম্বা, দামি optical link — diameter মাত্র কয়েক hop. এর পেছনে প্রযুক্তির পরিবর্তন: অনেক port-এর (high-radix) router chip সম্ভব হয়েছে, আর লম্বা তারের দাম মূলত optical transceiver-এ। আধুনিক কয়েকটি supercomputer-এর interconnect এই ধাঁচের বলে প্রকাশিত হয়েছে।
মূল উপলব্ধি: সেরা টপোলজি গণিত একা ঠিক করে না — router-এর port-সংখ্যা, তারের দাম আর দৈর্ঘ্য, আর প্রোগ্রামের যোগাযোগের ধরন মিলে ঠিক করে; প্রযুক্তি বদলালে উত্তরও বদলায়।
প্র ০৫একটি supercomputer HPL-এ peak-এর বড় অংশ পায়, অথচ অনেক বাস্তব প্রোগ্রাম পায় সামান্য কয়েক শতাংশ। Roofline আর “memory wall” দিয়ে এটি ব্যাখ্যা করুন — আর নকশাকারেরা কী করছেন?
১৯৯৫ সালে William Wulf আর Sally McKee একটি ছোট প্রবন্ধে সতর্ক করেন যে প্রসেসরের গতি মেমরির গতির চেয়ে অনেক দ্রুত বাড়ছে, তাই এক সময় প্রোগ্রামের গতি পুরোপুরি মেমরি ঠিক করবে — তাঁরা একে বলেন “memory wall”. তারপর core-সংখ্যা বাড়ায় সমস্যা আরও তীব্র হয়েছে: peak FLOP/s core আর SIMD lane-এর সাথে বাড়ে, কিন্তু chip-এর pin আর memory channel সেই হারে বাড়ে না। ফলে ridge point (peak ÷ bandwidth) প্রজন্মে প্রজন্মে ডানে সরছে — অর্থাৎ peak ছুঁতে প্রতি byte-এ আগের চেয়ে বেশি হিসাব লাগে।
HPL মূলত বড় dense matrix-এর factorisation, যার ভেতরে blocked matrix গুণ — operational intensity খুব উঁচু, ridge-এর অনেক ডানে। তাই ভালো বানানো যন্ত্রে HPL peak-এর বড় অংশ পায়। কিন্তু অনেক বাস্তব বৈজ্ঞানিক কোড sparse matrix, stencil বা গ্রাফ নিয়ে কাজ করে, যার I এক-এর নিচে। এই বৈষম্য দেখাতে HPCG নামে আরেকটি benchmark চালু হয়েছে (sparse linear solver-ধাঁচের), যেখানে একই শীর্ষ যন্ত্রগুলো সাধারণত peak-এর মাত্র কয়েক শতাংশ বা তারও কম পায় বলে প্রকাশিত ফলাফলে দেখা যায়। Roofline-এর ভাষায়: যন্ত্র একই, kernel-এর বিন্দু বাঁয়ে সরেছে, তাই ঢালু memory-ছাদের নিচে।
নকশাকারেরা কয়েক দিক থেকে লড়ছেন। মেমরিকে chip-এর কাছে আনা: GPU আর কিছু CPU-র পাশে একই package-এ স্তূপ করা HBM, যা সাধারণ DDR-এর চেয়ে বহু গুণ bandwidth দেয়। বড় on-chip cache, যাতে reuse বাড়ে। হিসাবকে মেমরির কাছে নেওয়া (processing-in-memory, near-memory computing) — গবেষণা আর কিছু বাণিজ্যিক পণ্যে। কম bit-এর সংখ্যা (fp16, bf16, int8) — একই byte-এ বেশি সংখ্যা, তাই কার্যত I বাড়ে; AI accelerator-এ এটিই প্রধান কৌশল। আর সফটওয়্যারে: blocking, temporal blocking, ডেটা-বিন্যাস বদল, communication-avoiding অ্যালগরিদম — একই উত্তর কম ডেটা সরিয়ে।
শক্তির দিক থেকেও একই বার্তা: ডেটা সরানোর energy হিসাবের energy-কে ছাড়িয়ে যায়, তাই ডেটা কম নাড়ানো দ্রুততা আর বিদ্যুৎ — দুটিই বাঁচায়।
মূল উপলব্ধি: আধুনিক সমান্তরাল যন্ত্রে FLOP সস্তা, byte দামি; একটি kernel-এর operational intensity জানলেই বোঝা যায় বাড়তি core কাজে লাগবে, না লাগবে বাড়তি bandwidth বা ভালো অ্যালগরিদম।
অনুশীলন
পরীক্ষার ধাঁচের সমস্যা — কাগজ-কলমে আগে নিজে করুন, তারপর পূর্ণ সমাধান মিলিয়ে নিন।
-
Amdahl আর Gustafson: একটি প্রোগ্রামের serial ভগ্নাংশ s = ০.০৫. (ক) ৩২ core-এ স্থির আকারের কাজে speedup আর efficiency কত? (খ) কাজ core-এর সাথে বাড়লে (s মাপা ৩২-core run-এ) scaled speedup কত? (গ) স্থির কাজে ৩২ core-এ speedup অন্তত ২০ পেতে serial ভগ্নাংশ সর্বোচ্চ কত হতে পারে?
(ক) S = ১ / (০.০৫ + ০.৯৫/৩২) = ১ / (০.০৫ + ০.০২৯৬৯) = ১ / ০.০৭৯৬৯ ≈ ১২.৫৫. Efficiency = ১২.৫৫ / ৩২ ≈ ৩৯%.
(খ) S = s + (১ − s)·N = ০.০৫ + ০.৯৫ × ৩২ = ০.০৫ + ৩০.৪ = ৩০.৪৫ (efficiency ≈ ৯৫%).
(গ) ১ / (s + (১ − s)/৩২) ≥ ২০ ⇒ s + (১ − s)/৩২ ≤ ০.০৫ ⇒ ৩২s + ১ − s ≤ ১.৬ ⇒ ৩১s ≤ ০.৬ ⇒ s ≤ ০.০১৯৪ (প্রায় ১.৯৪%). অর্থাৎ serial অংশ ৫% থেকে ২%-এর নিচে নামাতে হবে — core বাড়িয়ে নয়, কোড বদলে।
-
Topology আর routing হাতে: ২৫৬ node-এর জন্য (ক) ring, (খ) ১৬ × ১৬ mesh, (গ) ১৬ × ১৬ torus, (ঘ) ৮-মাত্রার hypercube — প্রতিটির diameter, link-সংখ্যা আর bisection (link-এ) বের করুন। (ঙ) ৪-মাত্রার hypercube-এ e-cube routing-এ 1010 থেকে 0111-এর পথ লিখুন।
(ক) Ring: diameter ২৫৬/২ = ১২৮, link ২৫৬, bisection ২.
(খ) Mesh (√N = ১৬): diameter ২(১৬ − ১) = ৩০, link ২ × ১৬ × ১৫ = ৪৮০, bisection √N = ১৬.
(গ) Torus: diameter ২ × ⌊১৬/২⌋ = ১৬, link ২N = ৫১২, bisection ২√N = ৩২.
(ঘ) Hypercube (n = ৮): diameter ৮, link (২৫৬/২) × ৮ = ১০২৪, bisection N/2 = ১২৮. (অংশ ৬-এর simulator-এ N = 256 বসিয়ে diameter আর link-সংখ্যা BFS দিয়ে মিলিয়েছি; bisection সেখানে সূত্র থেকেই বসে।)
(ঙ) 1010 ⊕ 0111 = 1101 → মাত্রা ০, ২, ৩ পেরোতে হবে, ৩ hop. Bit 0: 1010 → 1011; bit 1 মেলে, বাদ; bit 2: 1011 → 1111; bit 3: 1111 → 0111. পথ: 1010 → 1011 → 1111 → 0111.
-
Directory overhead: একটি ৩২-core ccNUMA যন্ত্রে ৩২ GiB মেমরি, cache line ৬৪ byte. Full bit-vector directory-র প্রতিটি entry-তে ৩২টি presence bit আর ২টি state bit. (ক) Overhead শতাংশে কত? (খ) মোট directory কত GiB? (গ) কত core হলে overhead ১০০% (ডেটার সমান) হবে? (ঘ) ৬৪-core-এ line ১২৮ byte করলে overhead কত (একই ২টি state bit)?
(ক) Line = ৬৪ × ৮ = ৫১২ bit. Entry = ৩২ + ২ = ৩৪ bit. Overhead = ৩৪ / ৫১২ ≈ ৬.৬৪%.
(খ) Block = ২৩৫ / ২৬ = ২২৯টি। মোট = ২২৯ × ৩৪ bit = ২২৯ × ৩৪ / ৮ byte = ৩৪ × ৬৪ MiB = ২১৭৬ MiB = ২.১২৫ GiB (৩২ GiB-এর ৬.৬৪%, মিলে গেছে)।
(গ) N + ২ = ৫১২ ⇒ N = ৫১০ core. এর বেশি হলে খাতা ডেটার চেয়েও বড় — তাই বড় যন্ত্রে sparse বা coarse directory.
(ঘ) Line = ১০২৪ bit, entry = ৬৬ bit ⇒ ৬৬ / ১০২৪ ≈ ৬.৪৫%. বড় line overhead কমায়, কিন্তু false sharing আর অপ্রয়োজনীয় ডেটা আনার ঝুঁকি বাড়ায়।
-
Roofline — আউটপুট অনুমান করুন: একটি অনুমিত যন্ত্রের peak ৫০০ GFLOP/s আর bandwidth ৫০ GB/s. JavaScript-এ
const roof = (p, b, i) => Math.min(p, b * i);ধরে নিচের লাইনগুলো কী ছাপবে, আর প্রতিটি kernel memory-bound না compute-bound বলুন। (ক) double-precision dot product (প্রতি উপাদানে ২ FLOP, দুটি ৮-byte পড়া):console.log(roof(500, 50, 2 / 16))(খ) I = ২০-এর একটি kernel:console.log(roof(500, 50, 20))(গ) float-এর b × b tile-matmul (I = b/৪) compute-bound হতে ন্যূনতম b কত?Ridge point = ৫০০ / ৫০ = ১০ FLOP/byte.
(ক) I = ২/১৬ = ০.১২৫; ৫০ × ০.১২৫ = ৬.২৫ < ৫০০ ⇒ ছাপবে
6.25— memory-bound, peak-এর মাত্র ১.২৫%.(খ) ৫০ × ২০ = ১০০০, min(৫০০, ১০০০) = ৫০০ ⇒ ছাপবে
500— compute-bound (I > ১০).(গ) b/৪ ≥ ১০ ⇒ b ≥ ৪০. তবে তিনটি tile cache-এ ধরতে হবে (৩ × ৪০ × ৪০ × ৪ = ১৯২০০ byte ≈ ১৮.৮ KiB) — সাধারণ L1-এ আঁটে, তাই বাস্তবসম্মত। বাস্তবে ২-এর ঘাত (৬৪) বেছে নেওয়া হয় কি না, তা মেপে ঠিক করুন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমDirectory-based coherence-এ P0 এমন একটি block পড়ছে, যার dirty কপি P3-এর cache-এ। ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- P0-র cache-এ miss; ঠিকানা থেকে block-এর home node ঠিক হয়
- P0 home node-কে read অনুরোধ পাঠায়
- Home directory দেখে: block dirty, মালিক P3
- Home অনুরোধটি মালিক P3-এর কাছে পাঠায়
- P3 ডেটা সরাসরি P0-কে পাঠায় আর নিজের কপি Shared করে
- Home directory-তে state Shared, presence-এ P0 আর P3
একটি প্রোগ্রামের ১০% serial. কাজের আকার স্থির রেখে ১০টি core-এ চালালে Amdahl-এর সূত্রে speedup প্রায় কত?
- ১০
- ৫.২৬
- ৯.১
- ১.১১
৬৪-core যন্ত্রে কাজের আকার core-এর সাথে বাড়ানো হয়, আর ৬৪-core run-এ মাপা serial ভগ্নাংশ ৫%. Gustafson-এর সূত্রে scaled speedup কত?
- ৬০.৮৫
- ১৫.৪
- ৬৪
- ২০
৬৪ node-এর একটি hypercube interconnect-এর diameter আর bisection bandwidth (link-সংখ্যায়) কত?
- diameter ৮, bisection ১৬
- diameter ১৪, bisection ৮
- diameter ৬, bisection ৩২
- diameter ৩২, bisection ২
৬৪-core যন্ত্রে ৬৪-byte cache line আর full bit-vector directory (প্রতি block-এ শুধু ৬৪টি presence bit, state bit বাদ). Directory-র storage overhead মেমরির কত শতাংশ?
- ১০০%
- ৫০%
- ১.৫৬%
- ১২.৫%
একটি যন্ত্রের peak ১০০০ GFLOP/s আর memory bandwidth ১০০ GB/s. যে kernel-এর operational intensity ০.২৫ FLOP/byte, roofline model অনুযায়ী তার সর্বোচ্চ সম্ভাব্য গতি কত?
- ১০০০ GFLOP/s, compute-bound
- ২৫ GFLOP/s, memory-bound
- ৪০০ GFLOP/s, memory-bound
- ২৫০ GFLOP/s, compute-bound
Linux-এ চলা একটি দুই-socket NUMA server-এ main thread একা একটি বড় array প্রথমবার শূন্য দিয়ে ভরে, তারপর দুই socket-এর ১৬টি thread array-টির আলাদা অংশে কাজ করে। ডিফল্ট first-touch নীতিতে কী ঘটে, আর সাধারণ প্রতিকার কী?
- Page দুই node-এ সমানভাবে ছড়িয়ে যায়, কোনো সমস্যা নেই
- Cache coherence protocol page-গুলোকে আপনাআপনি ঠিক node-এ সরিয়ে দেয়
- সব page main thread-এর node-এ বসে; প্রতিকার — প্রতিটি thread নিজের অংশ নিজে প্রথম ছোঁবে
- মেমরি বরাদ্দ ব্যর্থ হয়, তাই প্রোগ্রাম crash করে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- দুই scaling: স্থির কাজে Amdahl
1/(s + (1 − s)/N), সীমা ১/s, efficiency ≥ ৫০% শুধু N ≤ ১ + ১/s পর্যন্ত; বাড়ন্ত কাজে Gustafsons + (1 − s)N. - Flynn যথেষ্ট নয়: SISD, SIMD, MISD, MIMD-এর পর আসল প্রশ্ন মেমরি — shared (thread, OpenMP) না distributed (MPI); প্রোগ্রাম লেখার ধাঁচ প্রায় সবসময় SPMD.
- NUMA: দূরের মেমরি ধীর; Linux-এ first-touch — যে thread কাজ করবে, সে-ই ডেটা প্রথম ছোঁবে।
- Directory: home node-এ প্রতিটি block-এর presence bit; full bit-vector overhead (N + state) ÷ line-এর bit; dirty block-এর read miss ৩ hop.
- Network আর roofline: diameter, degree, link, bisection দিয়ে টপোলজির তুলনা (hypercube: log₂N, log₂N, (N/2)log₂N, N/2); attainable = min(peak, BW × I), ridge = peak ÷ BW.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫৫ · GPU আর AI accelerator — হাজার core, SIMT আর tensor unitপরবর্তী পর্বহাজার lane-এর যন্ত্র আর বিশেষায়িত hardware.
- পর্ব ৫৩ · Multicore আর cache coherence — MESI আর memory consistencyআগের পর্বSnooping, MSI/MESI, consistency আর atomic instruction.
- পর্ব ০৩ · পারফরম্যান্স মাপা — CPU time, CPI, MIPS, Amdahl-এর সূত্র আর benchmarkভিত্তিAmdahl-এর সূত্রের প্রথম পরিচয় আর benchmark-এর ফাঁদ।
- কম্পিউটার আর্কিটেকচার ও ডিজিটাল লজিক · কোর্স হোম৬২টি পর্বপুরো কোর্সের রোডম্যাপ — logic gate থেকে multicore আর GPU.
- অপারেটিং সিস্টেম · Multiprocessor আর real-time schedulingOS-এর দিকAffinity, NUMA-সচেতন scheduling আর load balancing.
- C প্রোগ্রামিং · Multithreadingহাতে-কলমেC-তে thread লিখে নিজের যন্ত্রে speedup মাপুন।