এই পর্বে যা শিখবেন
- Issue-slot diagram পড়া — vertical আর horizontal waste, আর কেন ILP একা এগুলো দূর করতে পারে না
- Hardware thread-এ কোন অংশ আলাদা (replicate) রাখতে হয় আর কোনটি ভাগ করা যায়
- Coarse-grained, fine-grained আর SMT — cycle ধরে কাজ, সুবিধা-অসুবিধা আর বাস্তব chip-এর উদাহরণ
- Coarse-grained-এর utilisation আর break-even-এর সূত্র, প্রমাণসহ; একটি issue-slot simulator চালিয়ে চার নীতির তুলনা
- OS-এর চোখে logical CPU আর sibling thread, cloud-এর vCPU, SMT-এর side channel আর প্রতিরক্ষা, আর GPU কীভাবে একই ধারণাকে চরমে নিয়ে যায়
১ · খালি issue slot — vertical আর horizontal waste
পর্ব ৩৯-এ দেখেছেন, একটি superscalar core প্রতি cycle-এ একাধিক instruction শুরু (issue) করতে পারে। ধরুন core-টি ৪-wide — প্রতি cycle-এ ৪টি issue slot. আদর্শ অবস্থায় IPC = ৪। পর্ব ৪০-এর out-of-order execution আর register renaming সেই লক্ষ্যের দিকে আরও এগোয়: প্রোগ্রামের ক্রমের বাইরে গিয়েও প্রস্তুত instruction খুঁজে আনে। তবু বাস্তবে সাধারণ প্রোগ্রামে গড় IPC প্রায়ই ৪-এর অনেক নিচে থাকে। কেন?
দুটি আলাদা কারণ। প্রথমটি মেমরি. একটি load যদি cache-এ না পায় (cache miss), ডেটা আনতে main memory পর্যন্ত যেতে হয় — আধুনিক chip-এ সেটা কয়েকশো cycle (পর্ব ৪৪-এ মেমরি হায়ারার্কির পুরো ছবি)। ওই load-এর ফলের ওপর নির্ভরশীল সব instruction ততক্ষণ বসে থাকে। Out-of-order core কিছু স্বাধীন কাজ এগিয়ে রাখে, কিন্তু তার জানালা (reorder buffer) একসময় ভরে যায়, আর তখন পুরো core থেমে থাকে। একে বলে memory wall-এর সমস্যা। দ্বিতীয় কারণ নির্ভরতা: এক cycle-এ চারটি slot ভরাতে চারটি পরস্পর-স্বাধীন instruction চাই, কিন্তু অনেক কোডে একসাথে দুটি-তিনটির বেশি পাওয়া যায় না।
একটি রেস্তোরাঁর রান্নাঘর ভাবুন, যেখানে চারটি চুলা (চারটি issue slot). একজন বাবুর্চি একটিমাত্র অর্ডার রাঁধছেন। কখনো রেসিপির ধাপ এমন যে একসাথে মাত্র দুটি চুলা লাগে — বাকি দুটি খালি জ্বলছে। আবার কখনো মসলা ফুরিয়েছে, বাজার থেকে আনতে লোক গেছে; ততক্ষণ চারটি চুলাই খালি। রান্নাঘর বড় করে (আরও চুলা) এই সমস্যা মেটে না। সমাধান: একসাথে কয়েকটি অর্ডার রাঁধা — এক অর্ডারের মসলা আসার অপেক্ষায় থাকলে অন্য অর্ডারের তরকারি চুলায় ওঠে। অর্ডারগুলো একে অন্যের ওপর নির্ভর করে না, তাই তাদের মেশানো সহজ। এটাই hardware multithreading-এর মূল ভাবনা।
১৯৯৫ সালে University of Washington-এর Dean Tullsen, Susan Eggers আর Henry Levy একটি বিখ্যাত গবেষণাপত্রে এই খালি জায়গাকে দুই ভাগে ভাগ করেছিলেন। একটি ছকে কলাম হলো issue slot, সারি হলো cycle; প্রতিটি ঘরে হয় একটি instruction, নয়তো খালি। এই ছকের নাম issue-slot diagram:
- Vertical waste — যে cycle-এ একটিও instruction issue হয়নি; ছকে পুরো সারি খালি। সাধারণত লম্বা stall-এর ফল: cache miss, TLB miss, branch-এর ভুল অনুমানের পরে pipeline ভরা।
- Horizontal waste — যে cycle-এ কিছু instruction issue হয়েছে, কিন্তু সব slot ভরেনি; সারির একাংশ খালি। সাধারণত একটি thread-এর ভেতরে যথেষ্ট স্বাধীন instruction না থাকার ফল — অর্থাৎ ILP-র সীমা।
W-wide core, C cycle, মোট issue হওয়া instruction I. তাহলে issue-slot utilisation = I ÷ (W × C), আর মোট খালি slot = W × C − I = W × (vertical waste-এর cycle) + (বাকি cycle-গুলোর খালি slot). গড় IPC = I ÷ C, যার সর্বোচ্চ মান W.
একটি ছোট হিসাব: ৪-wide core-এ একটি thread ৫০ cycle চলে (গড়ে প্রতি cycle-এ ২টি instruction), তারপর miss-এ ২০০ cycle থামে। প্রতি ২৫০ cycle-এ ১০০টি instruction: IPC = ০.৪, utilisation = ১০০ ÷ (৪ × ২৫০) = ১০% — ১০টি slot-এর ৯টিই খালি। চওড়া core বানালে মেমরির ২০০ cycle একটুও কমবে না।
ILP বনাম TLP — কাজ আনার দুটি উৎস
খালি slot ভরাতে স্বাধীন instruction চাই। সেগুলো আসতে পারে দুই জায়গা থেকে। Instruction-level parallelism (ILP) — একই instruction-ধারার ভেতরের স্বাধীন instruction, যা superscalar আর out-of-order hardware খুঁজে বের করে (পর্ব ৩৯–৪০)। Thread-level parallelism (TLP) — আলাদা instruction-ধারা, অর্থাৎ আলাদা thread বা প্রসেস, যাদের মধ্যে register-এর কোনো নির্ভরতা নেই। একটি ওয়েব সার্ভারে একসাথে শত শত অনুরোধ, একটি ফোনে একসাথে চলা অ্যাপ আর সার্ভিস, একটি compile-এর সময় আলাদা ফাইল — সবই TLP-র উৎস।
ILP খোঁজা ক্রমেই দামি হয়েছে; TLP-র নির্ভরতা-যাচাই প্রায় বিনামূল্যে, কারণ দুই thread-এর register আলাদা। তবে TLP পেতে software-কেই কাজকে thread-এ ভাগ করে দিতে হয়। Hardware multithreading হলো একটি core-এর ভেতরে TLP ব্যবহার করে ILP-র ঘাটতি পূরণ। একাধিক core দিয়ে TLP ব্যবহার — multicore — আসবে পর্ব ৫৩-এ।
২ · Hardware thread — কী আলাদা, কী ভাগ করা
OS-এর thread একটি software ধারণা: একটি নির্বাহ-ধারা, যার নিজের register-মান, PC আর stack আছে। একটি সাধারণ core একসাথে একটিমাত্র thread চালায়; অন্যটিতে যেতে OS-কে context switch করতে হয় — register মেমরিতে সেভ, অন্যটির register ফেরত, যা হাজার হাজার cycle-এর কাজ (Operating Systems কোর্সের Context switch পর্বে বিস্তারিত)। আর পর্ব ৩৮-এ দেখেছেন, এর শুরু একটি timer interrupt আর trap দিয়ে।
একটি hardware thread (বা hardware context) হলো core-এর ভেতরে একটি thread-এর architectural state রাখার একটি স্থায়ী ঘর। Core-এ যদি দুটি ঘর থাকে, তাহলে দুটি software thread একসাথে core-এ “বসে” থাকতে পারে, আর তাদের মধ্যে বদল করতে মেমরিতে কিছুই সেভ করতে হয় না — শুধু কোন ঘরের instruction আনা হবে, সেটি বদলালেই হলো। এই বদল OS জানেই না; এটি hardware-এর, cycle-এর হিসাবে।
| অংশ | সাধারণত | কেন |
|---|---|---|
| PC আর fetch-এর অবস্থা | প্রতি thread-এ আলাদা | প্রতিটি ধারা নিজের জায়গা থেকে instruction আনে |
| Architectural register (RISC-V-এর x1–x31, FP register) — বা out-of-order core-এ rename map | প্রতি thread-এ আলাদা; physical register-এর ভান্ডার প্রায়ই সবার ভাগে | দুই thread-এর x5 আলাদা জিনিস; rename map আলাদা হলে একই physical pool থেকে দুজনই নিতে পারে |
কিছু control/status register — privilege mode, trap-এর CSR, page table-এর ঠিকানা (RISC-V-এ satp, x86-এ CR3), interrupt-এর অবস্থা | প্রতি thread-এ আলাদা | OS-এর চোখে প্রতিটি hardware thread একটি পূর্ণ CPU; প্রতিটিতে আলাদা প্রসেস চলতে পারে |
| Return address stack, কিছু ছোট predictor-অবস্থা | প্রায়ই আলাদা বা thread-চিহ্ন দেওয়া | দুই thread-এর function call মিশে গেলে অনুমান নষ্ট হয় |
| Execution unit — ALU, multiplier, load/store, FP unit | ভাগ করা | এগুলোই দামি; ভাগ করাটাই multithreading-এর লক্ষ্য |
| L1 instruction ও data cache, L2 | ভাগ করা | দুই thread-এর ডেটা একই cache-এ জায়গার জন্য প্রতিযোগিতা করে |
| Branch predictor-এর বড় টেবিল | সাধারণত ভাগ করা (chip-ভেদে thread-চিহ্নসহ) | আলাদা করলে দ্বিগুণ জায়গা; ভাগ করলে একে অন্যের অনুমান নষ্ট করতে পারে |
| TLB | সাধারণত ভাগ করা, entry-তে thread বা address-space চিহ্নসহ — নকশাভেদে আলাদা | দুই thread আলাদা প্রসেসের হলে তাদের ঠিকানা-অনুবাদ মেশানো চলবে না |
| Reorder buffer, issue queue, load/store queue | নকশাভেদে: ভাগ করে ভাগাভাগি (partition) বা চাহিদামতো ভাগ (অংশ ৫) | একটি thread সব জায়গা দখল করলে অন্যটি না খেয়ে থাকে |
লক্ষ করুন, আলাদা রাখা অংশগুলো ছোট — কয়েক ডজন register আর কিছু CSR. দামি অংশগুলো (execution unit, cache, predictor) ভাগ করা। এজন্য একটি দ্বিতীয় hardware thread যোগ করার খরচ একটি দ্বিতীয় পূর্ণ core-এর চেয়ে অনেক কম। ঠিক কতটা, তা chip-ভেদে ভিন্ন আর নির্মাতারা সবসময় প্রকাশ করেন না — তাই কোনো নির্দিষ্ট শতাংশ মনে রাখার দরকার নেই। মূল কথা: multithreading মানে একটি core-কে দুটি core বানানো নয়; এক core-এর খালি সময় আর খালি slot অন্য thread-কে দেওয়া।
Software thread আর hardware thread গুলিয়ে ফেলবেন না। একটি প্রোগ্রাম ১০০টি software thread বানাতে পারে; core-এ হয়তো ২টি hardware thread, আর OS সেই ১০০টিকে পালা করে ২টি ঘরে বসায়। এই পর্বের বিষয় hardware-এর ঘর।
এবার প্রশ্ন: কখন কোন thread-এর instruction issue হবে? তিনটি প্রধান নীতি আছে, যাদের পার্থক্য এক কথায়: thread বদল কত ঘন ঘন আর কত সূক্ষ্মভাবে।
৩ · Coarse-grained multithreading — লম্বা stall-এ thread বদল
Coarse-grained multithreading-এ (একে switch-on-event multithreading-ও বলা হয়) core একটি thread-ই চালাতে থাকে, যতক্ষণ না একটি লম্বা stall আসে — সাধারণত last-level cache-এর miss. তখন core অন্য একটি প্রস্তুত hardware thread-এ চলে যায়। ছোট stall-এ (যেমন একটি load-use hazard, পর্ব ৩৬) বদল হয় না, কারণ বদলেরও দাম আছে।
দামটা কী? পর্ব ৩৫-এর pipeline মনে করুন। বদলের মুহূর্তে pipeline-এ পুরোনো thread-এর পরের instruction-গুলো ভরা — সেগুলো flush করতে হয়, আর নতুন thread-এর প্রথম instruction fetch থেকে issue পর্যন্ত পৌঁছাতে কয়েক cycle লাগে। এই refill-এর সময় কোনো instruction issue হয় না: পুরো সারি খালি, অর্থাৎ vertical waste. ধরুন এই switch penalty = S cycle. In-order pipeline-এ S মোটামুটি fetch থেকে issue পর্যন্ত ধাপের সংখ্যা; গভীর pipeline-এ বেশি।
নিচের stepper-এ ৪-wide একটি core, দুটি hardware thread A আর B. প্রতিটি thread-এর instruction কয়েকটি দলে (group) সাজানো: একটি দলের instruction পরস্পর-স্বাধীন, একসাথে issue হতে পারে; পরের দল আগেরটির ওপর নির্ভরশীল, তাই পরের cycle-এর আগে নয়। A-র দল: ৩, ২, ১ (এই load-টি miss করে), ৪. B-র দল: ২, ৩, ১ (miss), ২. Miss-এর latency ৫ cycle — load যে cycle-এ issue হয়, তার ৫ cycle পরে নির্ভরশীল দলটি চলতে পারে। Switch penalty S = ২ cycle. ছকে কলাম = cycle, সারি = slot; . = খালি slot, ~ = বদলের সময় নষ্ট slot.
| cycle | |
| core চালাচ্ছে | |
| A | |
| B | |
| issue / মোট slot |
- A একা চলছে। Cycle ০-তে A-র প্রথম দলের ৩টি, cycle ১-এ দ্বিতীয় দলের ২টি instruction issue হলো। B একটি hardware thread-এ বসে আছে, প্রস্তুত, কিন্তু coarse-grained নীতিতে stall না হলে বদল হয় না। ৮টি slot-এর ৫টি ভরা — বাকি ৩টি horizontal waste, যা এই নীতি কখনো ভরায় না।
- A-র load miss করল। Cycle ২-তে A-র তৃতীয় দল — একটি load — issue হলো, আর cache-এ পেল না। Miss latency ৫, তাই A-র পরের দল চলতে পারবে cycle ২ + ৫ = ৭-এ। এখন অপেক্ষা করলে cycle ৩ থেকে ৬ পুরো খালি যেত।
- বদলের দাম। Core miss দেখে B-তে বদল করে। A-র পরের instruction pipeline থেকে ফেলা হলো, B-র instruction fetch থেকে issue পর্যন্ত আসতে ২ cycle (cycle ৩ আর ৪) — এই দুই cycle-এ ৮টি slot নষ্ট (
~)। Vertical waste পুরো মোছেনি, শুধু ৪ cycle-এর জায়গায় ২ cycle হলো। - B চলছে। Cycle ৫-এ B-র প্রথম দলের ২টি, cycle ৬-এ দ্বিতীয় দলের ৩টি। এতক্ষণ A-র miss পেছনে মেমরি থেকে ডেটা আনছে — এটাই লাভ: miss-এর সময়ের একটা অংশে কাজ হলো। মোট ২৮টি slot-এ ১১টি instruction.
- A প্রস্তুত, তবু বদল নেই। Cycle ৭-এ A-র ডেটা এসে গেছে। কিন্তু B এখনো চলছে, আর coarse-grained শুধু stall-এ বদলায়। B-র তৃতীয় দল — তারও একটি load — cycle ৭-এ issue হয়ে miss করল; তার ডেটা আসবে cycle ৭ + ৫ = ১২-তে।
- আবার বদল, A ফিরল। Cycle ৮–৯ বদলের দাম, cycle ১০-এ A-র শেষ দলের ৪টি — পুরো সারি ভরা। ১১ cycle-এ ১৬টি instruction: utilisation ১৬ ÷ ৪৪ ≈ ৩৬%. Miss-এর অপেক্ষা ঢাকা পড়েছে, কিন্তু প্রতিটি বদলে ২ cycle আর প্রতিটি cycle-এর খালি slot রয়ে গেছে।
কতগুলো thread লাগবে, আর বদল কখন লাভজনক — সূত্র আর প্রমাণ
একটি সরল মডেল নিন, যা বিশ্ববিদ্যালয়ের পরীক্ষায় আর ইন্টারভিউতে প্রায়ই চাওয়া হয়। N-টি একই রকম thread; প্রতিটি R cycle পূর্ণ গতিতে চলে, তারপর একটি miss-এ L cycle অপেক্ষা; প্রতিটি বদলে S cycle নষ্ট। এখানে “utilisation” মানে যত cycle-এ core কাজের instruction চালাচ্ছে তার ভগ্নাংশ (cycle-এর ভেতরের horizontal waste আলাদা হিসাব)।
এক thread: U১ = R ÷ (R + L). N ≥ ২ thread, coarse-grained: UN = N·R ÷ max( N·(R + S), R + L + S ). সব miss ঢাকতে দরকারি thread-সংখ্যা N* = ১ + ⌈L ÷ (R + S)⌉, তখন U = R ÷ (R + S). আর N ≥ ২ হলে বদল এক thread-এর চেয়ে ভালো ⇔ S < L.
যুক্তি। Steady state-এ thread-গুলো পালা করে চলে: A চলে R, বদল S, B চলে R, বদল S, … তারপর আবার A. A-র miss শুরু হয় তার R শেষে, আর পরের বার A চলা শুরু করতে পারে তখনই, যখন (১) অন্য N − ১টি thread-এর পালা শেষ, এবং (২) A-র ডেটা এসে গেছে, আর তারপর বদলের S. প্রথম শর্তে A-র দুটি শুরুর মাঝে সময় N·(R + S); দ্বিতীয় শর্তে অন্তত R + L + S (চলা R, অপেক্ষা L, তারপর বদল S). তাই একটি পূর্ণ চক্রের দৈর্ঘ্য P = max(N·(R + S), R + L + S), আর প্রতি চক্রে N·R cycle কাজ — এখান থেকে UN. প্রথম রাশিটি বড় হলে অপেক্ষা পুরো ঢাকা: N·(R + S) ≥ R + L + S ⇔ (N − ১)(R + S) ≥ L ⇔ N ≥ ১ + L ÷ (R + S), আর পূর্ণসংখ্যায় N* = ১ + ⌈L ÷ (R + S)⌉. তখন U = R ÷ (R + S) — আর বাড়ানো যায় না, কারণ প্রতিটি R-এর পরে S দিতেই হবে।
শেষ দাবিটি: N ≥ ২ হলে UN > U১ হবে যদি দুটি অসমতাই খাটে — N·R ÷ N(R + S) > R ÷ (R + L), অর্থাৎ L > S; আর N·R ÷ (R + L + S) > R ÷ (R + L), অর্থাৎ (N − ১)(R + L) > S. S < L হলে দ্বিতীয়টি আপনা থেকেই খাটে, কারণ R + L > L > S. তাই শর্ত একটিই: বদলের দাম miss-এর latency-র চেয়ে কম হতে হবে। এজন্যই coarse-grained শুধু লম্বা miss-এ বদলায়, L1 miss বা ছোট hazard-এ নয়। এই সূত্রগুলো একটি cycle-ধরে-চলা simulator দিয়ে মিলিয়ে দেখা হয়েছে (চারটি ভিন্ন R, L, S-এর জন্য ১ থেকে ৫টি thread).
এবার নিচের calculator চালিয়ে দেখুন। তিনটি পরিস্থিতি: DRAM miss আর ছোট refill; একই miss কিন্তু গভীর pipeline-এ বড় refill; আর একটি L2 hit, যার latency বদলের দামের চেয়ে কম।
// Coarse-grained multithreading: does switching on a miss pay off?
// R = cycles a thread runs between misses, L = miss latency, S = switch cost (refill), N = threads
function util(N, R, L, S) {
if (N === 1) return R / (R + L); // one thread: run R, wait L, repeat (no switch)
return N * R / Math.max(N * (R + S), R + L + S); // a round of N runs + N switches, or wait for the miss
}
const cases = [
{ name: "DRAM miss, short refill", R: 50, L: 200, S: 10 },
{ name: "deep pipeline refill", R: 50, L: 200, S: 30 },
{ name: "L2 hit, costly switch", R: 50, L: 8, S: 10 }];
for (const c of cases) {
const need = 1 + Math.ceil(c.L / (c.R + c.S)); // smallest N with (N-1)(R+S) >= L
const best = c.R / (c.R + c.S), alone = util(1, c.R, c.L, c.S);
console.log(`${c.name}: R=${c.R} L=${c.L} S=${c.S}`);
console.log(" N " + [1, 2, 3, 4, 5, 6].map(n => String(n).padStart(7)).join(""));
console.log(" util " + [1, 2, 3, 4, 5, 6].map(n => ((100 * util(n, c.R, c.L, c.S)).toFixed(1) + "%").padStart(7)).join(""));
console.log(` threads to hide the miss = ${need}, ceiling R/(R+S) = ${(100 * best).toFixed(1)}%` +
(best < alone ? ` -> worse than one thread (${(100 * alone).toFixed(1)}%): S > L, do not switch` : ""));
}আসল আউটপুট:
DRAM miss, short refill: R=50 L=200 S=10
N 1 2 3 4 5 6
util 20.0% 38.5% 57.7% 76.9% 83.3% 83.3%
threads to hide the miss = 5, ceiling R/(R+S) = 83.3%
deep pipeline refill: R=50 L=200 S=30
N 1 2 3 4 5 6
util 20.0% 35.7% 53.6% 62.5% 62.5% 62.5%
threads to hide the miss = 4, ceiling R/(R+S) = 62.5%
L2 hit, costly switch: R=50 L=8 S=10
N 1 2 3 4 5 6
util 86.2% 83.3% 83.3% 83.3% 83.3% 83.3%
threads to hide the miss = 2, ceiling R/(R+S) = 83.3% -> worse than one thread (86.2%): S > L, do not switch
প্রথম ক্ষেত্রে একটি thread-এ মাত্র ২০% (৫০ ÷ ২৫০)। প্রতিটি নতুন thread প্রায় ১৯ শতাংশ-বিন্দু যোগ করে, কারণ চক্রের দৈর্ঘ্য তখনো R + L + S = ২৬০ — সংখ্যাটি N·৫০ ÷ ২৬০: ৩৮.৫%, ৫৭.৭%, ৭৬.৯%. পাঁচটি thread-এ N·(R + S) = ৩০০ > ২৬০, miss পুরো ঢাকা, আর utilisation আটকে যায় ৫০ ÷ ৬০ = ৮৩.৩%-এ; ষষ্ঠ thread কিছুই যোগ করে না। সূত্রও তাই বলে: N* = ১ + ⌈২০০ ÷ ৬০⌉ = ১ + ৪ = ৫. দ্বিতীয় ক্ষেত্রে বদলের দাম ৩০, তাই সিলিং নেমে ৫০ ÷ ৮০ = ৬২.৫%, আর চারটি thread-এই পৌঁছে যায় (১ + ⌈২০০ ÷ ৮০⌉ = ৪)। তৃতীয় ক্ষেত্রটি সতর্কবার্তা: miss মাত্র ৮ cycle, বদল ১০ — বদল করলে ৮৩.৩%, না করলে ৮৬.২%. Thread যত বেশিই থাকুক, বদল এখানে ক্ষতি। বাস্তব coarse-grained নকশা তাই শুধু সবচেয়ে লম্বা ঘটনায় বদলায়।
Coarse-grained-এর সুবিধা: একক thread-এর গতি প্রায় অক্ষুণ্ণ, hardware সরল। সীমাবদ্ধতা: বদলের দাম, আর horizontal waste একটুও কমে না। বাস্তবে IBM-এর কিছু server processor (১৯৯০-এর দশকের শেষে AS/400 আর RS/6000-এর জন্য বানানো “Northstar”/RS64 পরিবার) আর Intel Itanium-এর একটি প্রজন্ম (Montecito, ২০০৬ সালের দিকে) এই ধরনের switch-on-event multithreading ব্যবহার করেছিল বলে জানা যায়।
৪ · Fine-grained multithreading — প্রতি cycle-এ অন্য thread
Fine-grained multithreading-এ (interleaved multithreading) core প্রতি cycle-এ পালা করে অন্য thread-এর instruction issue করে — round-robin. যে thread এখন miss-এর অপেক্ষায়, সাধারণত তাকে বাদ দিয়ে পরের প্রস্তুত thread-কে পালা দেওয়া হয়। বদলের কোনো দাম নেই, কারণ pipeline-এ সব সময়ই কয়েকটি thread-এর instruction মিশে থাকে; প্রতিটি instruction-এর সাথে একটি ছোট thread-নম্বর চলে, যা বলে কোন register set পড়তে বা লিখতে হবে।
গার্মেন্টস কারখানার একটি সেলাই মেশিন ভাবুন, যার সামনে তিনজন কর্মী পালা করে বসেন — প্রত্যেকে এক ফোঁড় দিয়ে সরে যান, পরের জন বসেন। একজনের কাপড় শেষ হলে (stall), তিনি কাপড় আনতে যান আর তাঁর পালা বাদ পড়ে; মেশিন থামে না। মেশিন কখনো অলস থাকে না, যদি অন্তত একজন প্রস্তুত থাকেন — কিন্তু প্রত্যেক কর্মীর নিজের জামা শেষ হতে তিন গুণ সময় লাগে।
এই উপমাতেই fine-grained-এর মূল trade-off: vertical waste প্রায় শূন্য (অন্তত একটি thread প্রস্তুত থাকলেই cycle কাজে লাগে), কিন্তু একক thread-এর গতি কমে — N-টি প্রস্তুত thread থাকলে প্রতিটি পায় প্রতি N cycle-এ একবার। আর একটি cycle-এ মাত্র একটি thread issue করে, তাই horizontal waste থেকেই যায়। নিচের stepper-এ তিনটি thread A, B, C; ৪-wide core; miss latency ৪. A-র দল: ২, ১ (miss), ৩. B-র দল: ১, ২, ১ (miss). C-র দল: ৩, ১, ২.
| cycle | |
| পালা কার | |
| কে issue করল | |
| A | |
| B | |
| issue / মোট slot |
- Cycle ০ — A-র পালা। A-র প্রথম দলে ২টি স্বাধীন instruction, তাই ৪টি slot-এর ২টি ভরল। একটি cycle-এ শুধু একটি thread — বাকি দুই slot অন্য thread-এর কাজ পেলেও নিতে পারত, কিন্তু fine-grained নীতি তা দেয় না।
- Cycle ১ — B-র পালা। B-র প্রথম দলে মাত্র ১টি instruction; ৩টি slot খালি (horizontal waste). লক্ষ করুন, A-র দ্বিতীয় দল প্রস্তুত, কিন্তু পালা B-র।
- Cycle ২ — C-র পালা। C-র প্রথম দলে ৩টি। এখন পর্যন্ত ১২টি slot-এ ৬টি instruction, কোনো cycle পুরো খালি নয়।
- Cycle ৩ — A-র load miss করল। A-র দ্বিতীয় দল একটি load, যা miss করে; latency ৪, তাই A আবার প্রস্তুত হবে cycle ৩ + ৪ = ৭-এ। একক thread হলে cycle ৪, ৫, ৬ পুরো খালি যেত।
- Cycle ৪ আর ৫ — B, তারপর C. B-র দ্বিতীয় দল ২টি, C-র দ্বিতীয় দল ১টি। A-র miss চলছে, কিন্তু core একটি cycle-ও নষ্ট করেনি: vertical waste শূন্য। দাম? প্রতি thread তিন cycle-এ মাত্র একবার সুযোগ পাচ্ছে।
- Cycle ৬ আর ৭ — অপেক্ষমাণ thread বাদ। Cycle ৬-এ পালা A-র, কিন্তু A miss-এর অপেক্ষায় — তাকে বাদ দিয়ে B (তার তৃতীয় দল, একটি load, যেটিও miss করে; ফিরবে cycle ১০-এ)। Cycle ৭-এ A প্রস্তুত, কিন্তু ক্রমে পরের জন C — C-র ২টি। ৮ cycle-এ ১৩টি instruction, utilisation ১৩ ÷ ৩২ ≈ ৪১%: একটি cycle-ও খালি নয়, কিন্তু slot-এর অর্ধেকের বেশি খালি।
Barrel processor — চরম fine-grained
Fine-grained-এর সবচেয়ে কঠোর রূপ barrel processor: N-টি thread কঠোরভাবে পালা করে, কোনো thread বাদ যায় না (অপেক্ষমাণ হলে তার পালা খালি যায়)। এর একটি চমৎকার সুবিধা আছে। যদি N ≥ pipeline-এর ধাপের সংখ্যা হয়, তাহলে একই thread-এর পরপর দুটি instruction pipeline-এ কখনো একসাথে থাকে না — আগেরটি শেষ হলে তবে পরেরটি ঢোকে। তখন data hazard নেই, forwarding লাগে না, branch-এর অনুমানও লাগে না (পর্ব ৩৬–৩৭-এর পুরো জটিলতা উধাও)। Pipeline খুব সরল হয়; দাম একক thread-এর গতি — প্রতিটি thread চলে clock-এর ১/N গতিতে।
ইতিহাসে এর কয়েকটি বিখ্যাত উদাহরণ: ১৯৬৪ সালের CDC 6600-এর দশটি “peripheral processor” আসলে একটি ভাগ করা hardware, যা পালা করে দশটি processor-এর অবস্থা ঘুরিয়ে চালাত — ছবিতে একে ঘুরন্ত একটি “barrel” হিসেবে দেখানো হতো, নামটা সেখান থেকেই বলে প্রচলিত। ১৯৮০-এর দশকে Burton Smith-এর Denelcor HEP আর পরে Tera MTA (১৯৯০-এর দশক) প্রতি processor-এ বহু hardware thread রেখে মেমরির latency লুকাত — Tera MTA প্রতি processor-এ ১২৮টি thread রাখত বলে উল্লেখ পাওয়া যায়, আর data cache প্রায় ছিলই না। ২০০৫ সালের দিকে Sun-এর UltraSPARC T1 (“Niagara”) ৮টি সরল in-order core-এর প্রতিটিতে ৪টি thread রেখে fine-grained পদ্ধতিতে চালাত — ওয়েব সার্ভারের মতো কাজে throughput-এর জন্য। এসব নকশার সংখ্যা সূত্রভেদে কিছুটা আলাদা বর্ণিত হয়।
৫ · Simultaneous multithreading (SMT) — একই cycle-এ কয়েকটি thread
Fine-grained vertical waste মোছে, কিন্তু horizontal waste রাখে; superscalar horizontal waste-এর বিরুদ্ধে লড়ে, কিন্তু vertical waste-এ অসহায়। Tullsen আর সহকর্মীদের প্রশ্ন ছিল: দুটো একসাথে কেন নয়? Simultaneous multithreading (SMT)-এ একটি out-of-order superscalar core একই cycle-এ একাধিক thread-এর instruction issue করে। যে slot একটি thread ভরাতে পারছে না, অন্য thread-এর প্রস্তুত instruction তা নেয়।
Out-of-order core-এ এর জন্য খুব বেশি নতুন যন্ত্র লাগে না। পর্ব ৪০-এ দেখেছেন, register renaming প্রতিটি architectural register-কে একটি physical register-এ বসায়, আর issue queue শুধু দেখে কোন instruction-এর operand তৈরি — “কোন thread-এর” প্রশ্নই নেই। তাই SMT-তে মূলত লাগে: প্রতি thread-এ আলাদা PC, আলাদা rename map, আলাদা retire (পর্ব ৩৮-এর precise exception প্রতি thread-এ আলাদা), আর ভাগাভাগির নিয়ম।
নিচের stepper-এ আবার coarse-grained-এর একই দুটি thread A আর B (A: ৩, ২, ১ miss, ৪; B: ২, ৩, ১ miss, ২; miss latency ৫)। প্রতি cycle-এ অগ্রাধিকার পালা করে: জোড় cycle-এ আগে A, বিজোড়-এ আগে B; আগের জন যতটা পারে নেয়, বাকি slot পরের জন। একটি thread এক cycle-এ শুধু তার এখনকার দলটিই issue করতে পারে।
| cycle | |
| আগে কে | |
| issue | |
| A | |
| B | |
| issue / মোট slot |
- Cycle ০ — দুজন একসাথে। অগ্রাধিকার A-র: A-র প্রথম দলের ৩টি। চতুর্থ slot-টি একক thread-এ খালি যেত; SMT-তে B তার প্রথম দলের (২টি) একটি সেখানে বসায়। B-র দলের ১টি বাকি থাকল, পরের cycle-এ যাবে। প্রথম সারিই পুরো ভরা।
- Cycle ১ — এবার আগে B. B তার দলের বাকি ১টি, A তার দ্বিতীয় দলের ২টি — মোট ৩টি, একটি slot খালি। B-র পরের দল এই দলের ওপর নির্ভরশীল, তাই একই cycle-এ যেতে পারে না। ৮টি slot-এ ৭টি।
- Cycle ২ — A-র load miss. A-র তৃতীয় দল (load, ১টি) আর B-র দ্বিতীয় দলের ৩টি — আবার পুরো সারি। A-র load miss করে; ডেটা আসবে cycle ২ + ৫ = ৭-এ। লক্ষ করুন, কোনো বদলের দাম নেই — A শুধু issue করা থামাবে।
- Cycle ৩ — B-র load-ও miss. B-র তৃতীয় দল একটি load; সেটিও miss করে, ডেটা cycle ৩ + ৫ = ৮-এ। এই cycle-এ শুধু ১টি instruction — ৩টি slot খালি, কারণ A অপেক্ষায়।
- Cycle ৪ থেকে ৬ — দুজনেই অপেক্ষায়। দুটি thread-এর miss একসাথে চলছে, তাই ৩টি পুরো খালি cycle — vertical waste. SMT তখনই slot ভরাতে পারে, যখন অন্তত একটি thread-এর হাতে প্রস্তুত কাজ আছে; দুটি thread-ই একই সময়ে miss করলে লাভ নেই। আরও thread থাকলে এই ফাঁকও ভরত।
- Cycle ৭ — A ফিরল। অগ্রাধিকার B-র, কিন্তু B অপেক্ষায়; A-র শেষ দলের ৪টি। ৮ cycle-এ ১৬টি instruction, utilisation ৫০%. Coarse-grained-এ একই ১৬টি instruction-এ লেগেছিল ১১ cycle (৩৬%) — SMT বদলের দামও দেয়নি, আর একক thread-এর খালি slot-ও কাজে লাগিয়েছে।
বাস্তবের SMT
Tullsen-দের গবেষণার পর DEC তাদের পরের প্রজন্মের Alpha 21464-এ চারটি thread-এর SMT-র পরিকল্পনা করেছিল, কিন্তু কোম্পানি বিক্রি হওয়ার পর প্রকল্পটি বাতিল হয় (২০০১ সালের দিকে)। প্রথম বাণিজ্যিক SMT আসে Intel-এর কাছ থেকে: Hyper-Threading নামে ২০০২ সালের দিকে Xeon server chip-এ, তারপর একই বছরের শেষে Pentium 4-এ — প্রতি core-এ দুটি hardware thread. IBM ২০০৪ সালের দিকে POWER5-এ দুটি thread-এর SMT আনে; পরে POWER7-এ চারটি (SMT4) আর POWER8 থেকে আটটি (SMT8) পর্যন্ত, আর OS চালু অবস্থাতেই SMT-এর মাত্রা বদলানো যায় বলে জানা যায়। AMD ২০১৭ সালের Zen থেকে দুটি thread-এর SMT দেয়। অন্যদিকে Apple-এর M-series আর অনেক ফোনের ARM core-এ SMT নেই, আর ২০২৪ সালের কিছু Intel ল্যাপটপ chip-এর বড় core থেকেও Hyper-Threading বাদ দেওয়া হয়েছে বলে খবর — কেন, তা ভাবনার প্রশ্ন ৫-এ।
ভাগাভাগি — partition না প্রতিযোগিতা?
SMT-র সবচেয়ে সূক্ষ্ম নকশা-প্রশ্ন হলো ভেতরের সারিগুলো (reorder buffer, issue queue, load/store queue, physical register) কীভাবে ভাগ হবে। দুটি চরম পথ:
স্থির ভাগ (static partitioning)
প্রতিটি thread অর্ধেক পায়। সরল আর ন্যায্য — একটি thread অন্যটিকে কখনো না খাইয়ে রাখতে পারে না। কিন্তু একটি thread অলস থাকলে তার অর্ধেক নষ্ট হয়, যদি না chip একক-thread অবস্থায় পুরোটা একজনকে দেয়। Intel-এর প্রথম Hyper-Threading নকশায় কয়েকটি সারি এভাবে ভাগ করা ছিল বলে Intel-এর প্রকাশিত বর্ণনায় পাওয়া যায়।
চাহিদামতো ভাগ (dynamic sharing)
যে আগে চায়, সে পায়। Utilisation বেশি, কিন্তু বিপদ আছে: একটি thread miss-এ আটকে থেকে তার অপেক্ষমাণ instruction দিয়ে পুরো issue queue ভরে ফেলতে পারে — অন্য thread প্রস্তুত থেকেও জায়গা পায় না। তাই বাস্তবে প্রায়ই সীমা বসানো হয় (প্রতি thread সর্বোচ্চ এতটা), বা fetch-এর নীতি দিয়ে নিয়ন্ত্রণ করা হয়।
Tullsen-দের ১৯৯৬ সালের পরের একটি গবেষণা দেখায়, fetch-এর অগ্রাধিকারই সবচেয়ে কার্যকর লিভার: যে thread-এর কম instruction এখন pipeline-এ আটকে আছে, তাকে আগে fetch করতে দেওয়া (নাম ICOUNT). আজকের chip-গুলোর ঠিক নীতি সাধারণত প্রকাশিত নয়।
আরও গভীরে: SMT শুধু issue-এর বিষয় নয়। Fetch-এ প্রতি cycle-এ এক বা দুটি thread থেকে আনা হয়, decode আর rename প্রায়ই পালা করে চলে (অর্থাৎ সেখানে fine-grained), আর শুধু issue আর execute-এ পুরো “simultaneous” মেশানো হয়। Retire-এও প্রায়ই পালা করে। অনেক chip একটি thread অলস হলে (OS যখন সেটিকে halt-এর মতো অবস্থায় রাখে) স্থির ভাগ করা সারিগুলো একক thread-কে পুরো দিয়ে দেয়, যাতে একক-thread-এর গতি না কমে। আর IBM POWER-এর মতো কিছু নকশায় OS চালু অবস্থাতেই SMT1, SMT2, SMT4 বা SMT8 বেছে নেওয়া যায় — কাজের ধরন বুঝে।
৬ · চার নীতি পাশাপাশি — simulator আর throughput বনাম latency
এবার Tullsen-দের বিখ্যাত ছবিটি — চারটি নীতি পাশাপাশি। নিচের ছবি অংশের simulator-এর প্রথম ১০ cycle থেকে আঁকা: তিনটি thread A, B, C, ৪-wide core, miss latency ৬, coarse-grained-এ বদলের দাম ২ cycle. ওপর থেকে নিচে cycle, বাঁ থেকে ডানে ৪টি slot.
নিচের simulator নিজে চালিয়ে দেখুন। প্রতিটি thread-এর instruction-ধারা কয়েকটি দলে লেখা ("3 2 1m 4 2" মানে প্রথমে ৩টি স্বাধীন, তারপর ২টি, তারপর ১টি load যা miss করে, …), আর ধারা শেষ হলে আবার শুরু থেকে ঘোরে। আউটপুটে utilisation, প্রতিটি thread-এর IPC, মোট IPC, খালি cycle (vertical waste) আর ভরা cycle-এর ভেতরের খালি slot (horizontal waste) — সাথে প্রথম ২৪ cycle-এর slot-ছক (কলাম = cycle). Miss latency এখানে ইচ্ছা করে ছোট (৬), যাতে ছকে দেখা যায়; বাস্তবে DRAM miss কয়েকশো cycle.
// Issue-slot simulator: 3 threads on a 4-wide core under 4 policies (single, coarse, fine, SMT)
const W = 4, MISS = 6, SW = 2, CYC = 48, SHOW = 24; // width, miss latency, coarse switch cost, cycles run / drawn
const prog = { A: "3 2 1m 4 2", B: "1 1m 3 2 1", C: "2 4 1 1 1m" }; // groups of independent instrs; m = cache miss
function run(policy) {
const T = Object.entries(prog).map(([id, s]) => ({ id, g: s.split(" "), pc: 0, left: parseInt(s), readyAt: 0, done: 0 }));
const rows = []; let cur = 0, swUntil = 0, rr = 0;
const issue = (t, c, room) => { // issue up to `room` instrs of t's current group
const k = Math.min(room, t.left); t.left -= k; t.done += k; t.readyAt = c + 1;
if (t.left === 0) { // group finished; next group depends on it
if (t.g[t.pc].endsWith("m")) t.readyAt = c + MISS;
t.pc = (t.pc + 1) % t.g.length; t.left = parseInt(t.g[t.pc]);
}
return t.id.repeat(k);
};
for (let c = 0; c < CYC; c++) {
let s = "";
if (policy === "single") { if (T[0].readyAt <= c) s = issue(T[0], c, W); }
else if (policy === "coarse") {
if (c < swUntil) s = "~".repeat(W); // pipeline refill after a switch: dead cycle
else if (T[cur].readyAt <= c) s = issue(T[cur], c, W);
else { const nx = [1, 2].map(k => T[(cur + k) % T.length]).find(t => t.readyAt <= c); // next ready thread, in turn
if (nx) { cur = T.indexOf(nx); swUntil = c + SW; s = "~".repeat(W); } }
} else if (policy === "fine") { // one thread per cycle, round-robin, skip stalled ones
for (let k = 0; k < T.length && !s; k++) { const t = T[(rr + k) % T.length]; if (t.readyAt <= c) { s = issue(t, c, W); rr = T.indexOf(t) + 1; } }
} else { // SMT: fill the slots from every ready thread
for (let k = 0; k < T.length; k++) { const t = T[(rr + k) % T.length]; if (t.readyAt <= c && s.length < W) s += issue(t, c, W - s.length); }
rr = (rr + 1) % T.length;
}
rows.push(s.padEnd(W, "."));
}
const used = T.reduce((a, t) => a + t.done, 0), busy = rows.filter(r => /[A-C]/.test(r));
console.log(`${policy.padEnd(6)} util ${(100 * used / (W * CYC)).toFixed(1).padStart(5)}% IPC ${T.map(t => t.id + "=" + (t.done / CYC).toFixed(2)).join(" ")} total=${(used / CYC).toFixed(2)} empty cycles=${CYC - busy.length} empty slots in busy cycles=${busy.join("").split(".").length - 1}`);
for (let i = 0; i < W; i++) console.log(` slot${i} ` + rows.slice(0, SHOW).map(r => r[i]).join(""));
}
["single", "coarse", "fine", "smt"].forEach(run);আসল আউটপুট:
single util 28.1% IPC A=1.13 B=0.00 C=0.00 total=1.13 empty cycles=25 empty slots in busy cycles=38
slot0 AAA.....AAAAA.....AAAAA.
slot1 AA......AAAA......AAAA..
slot2 A.......A.A.......A.A...
slot3 ........A.........A.....
coarse util 33.9% IPC A=0.63 B=0.35 C=0.38 total=1.35 empty cycles=14 empty slots in busy cycles=71
slot0 AAA~~BB~~CCCCC~~AAAAA~~B
slot1 AA.~~..~~CC...~~AAAA.~~B
slot2 A..~~..~~.C...~~A.A..~~B
slot3 ...~~..~~.C...~~A....~~.
fine util 47.4% IPC A=0.75 B=0.54 C=0.60 total=1.90 empty cycles=0 empty slots in busy cycles=101
slot0 ABCABCACCCBBABABCABCACAC
slot1 A.CA.C....BBA.A.CA.CA...
slot2 A....C....B.A....A.C....
slot3 .....C......A......C....
smt util 67.2% IPC A=1.06 B=0.71 C=0.92 total=2.69 empty cycles=1 empty slots in busy cycles=59
slot0 ABCACC.BBBBCACCACBBBBBCC
slot1 ACCCA..BB.ACACAAABB...CC
slot2 ACC....B..AACA.C.B....AC
slot3 BAC.......ABCA........AC
মোট IPC ধাপে ধাপে বাড়ে: ১.১৩ → ১.৩৫ → ১.৯০ → ২.৬৯. Single-thread-এ ৪৮টি cycle-এর ২৫টিই পুরো খালি — A-র প্রতিটি miss-এ ৫টি খালি cycle (slot0-এর .....)। Coarse-grained খালি cycle ১৪-তে নামায়, কিন্তু সেগুলোর বেশির ভাগই এখন বদলের ~~; আর ভরা cycle-এর খালি slot বেড়ে ৭১ — কারণ বেশি cycle এখন কাজের, অথচ প্রতিটি cycle-এ এখনো একটিই thread. Fine-grained-এ খালি cycle শূন্য, কিন্তু খালি slot ১০১ — এক cycle-এ এক thread-এর দলে প্রায়ই ১ বা ২টির বেশি instruction নেই। SMT দুটোকেই কমায়: খালি cycle মাত্র ১ (cycle ৬-এ তিনটি thread-ই অপেক্ষায় ছিল — slot0-এর সপ্তম ঘরে .), খালি slot ৫৯, utilisation ৬৭.২%.
এবার প্রতি thread-এর IPC দেখুন। A একা চললে ১.১৩; SMT-তে ১.০৬, fine-grained-এ ০.৭৫, coarse-grained-এ ০.৬৩. অর্থাৎ মোট কাজ বাড়ে, কিন্তু প্রতিটি thread একা চলার চেয়ে ধীর। এই মডেলে SMT-তে A-র ক্ষতি সামান্য, কারণ slot ছাড়া কিছু ভাগ হচ্ছে না; বাস্তবে cache আর ALU-র লড়াইয়ে ক্ষতি বেশি হতে পারে।
Throughput বনাম single-thread latency
এখান থেকে multithreading-এর মূল trade-off পরিষ্কার। পর্ব ০৩-এর ভাষায়: throughput (প্রতি সেকেন্ডে মোট কতটা কাজ) বাড়ে, কিন্তু একটি নির্দিষ্ট কাজের latency (শুরু থেকে শেষ) কমে না — বরং সাধারণত বাড়ে। একটি ওয়েব সার্ভার, যেখানে প্রতি সেকেন্ডে কতগুলো অনুরোধ সামলানো যায় সেটাই মাপকাঠি, এতে লাভবান। কিন্তু একটি খেলা বা একটি real-time নিয়ন্ত্রক, যেখানে একটি নির্দিষ্ট thread-এর দেরিই সমস্যা, সেখানে sibling thread-এর উপস্থিতি ক্ষতিকর হতে পারে। নিচের টেবিলে সারাংশ:
| নীতি | কখন thread বদল | Vertical waste | Horizontal waste | একক thread-এর গতি | Hardware-এর জটিলতা |
|---|---|---|---|---|---|
| Single-thread superscalar | কখনো না (OS-এর context switch ছাড়া) | বেশি | বেশি | সেরা | ভিত্তি |
| Coarse-grained | লম্বা stall-এ | কমে, কিন্তু বদলের দাম যোগ হয় | বদলায় না | প্রায় অক্ষুণ্ণ | কম |
| Fine-grained | প্রতি cycle-এ | প্রায় শূন্য (একটি thread প্রস্তুত থাকলে) | বদলায় না | অনেক কমে (প্রায় ১/N) | কম; barrel-এ hazard-যুক্তিও বাদ |
| SMT | বদল বলে কিছু নেই — একই cycle-এ মেশানো | কমে | কমে | কিছুটা কমে, কাজভেদে | বেশি — out-of-order core-এর ওপর |
SMT কখন সাহায্য করে, কখন ক্ষতি
SMT-র লাভ নিয়ে সবচেয়ে সৎ উত্তর: কাজের ধরনভেদে খুব আলাদা — বেশ বড় থেকে প্রায় শূন্য, এমনকি ঋণাত্মক। কোনো একটি শতাংশকে “SMT-র লাভ” হিসেবে মনে রাখবেন না। বরং চিনে রাখুন কোন বৈশিষ্ট্যে লাভ হয়:
সাধারণত সাহায্য করে
Latency-নির্ভর কাজ: pointer ধরে ধরে চলা (linked list, tree, hash table), ঘন ঘন cache miss, ভুল branch অনুমান — একটি thread থেমে থাকলে অন্যটি slot পায়। মিশ্র কাজ: একটি thread মূলত integer, অন্যটি floating point — তারা আলাদা unit চায়। অনেক ছোট স্বাধীন অনুরোধ: ডেটাবেস, ওয়েব সার্ভার, compile.
কম সাহায্য বা ক্ষতি
দুজনেই একই unit চায়: ভালোভাবে সাজানো ঘন ম্যাট্রিক্স গুণ বা SIMD-ভারী কোড (পর্ব ৪২) একাই FP unit ভরিয়ে রাখে। Cache-এর লড়াই: দুই thread-এর মিলিত working set L1 বা L2-তে না ধরলে একে অন্যের ডেটা সরিয়ে দেয়, miss বাড়ে। Memory bandwidth শেষ: মেমরির পথ ভরা থাকলে বাড়তি thread বাড়তি অপেক্ষা আনে। একটি thread-এর latency জরুরি হলে।
এজন্য কিছু high-performance computing ক্লাস্টার SMT বন্ধ রাখে বা প্রতি core-এ একটি কাজই চালায়, আবার ডেটাবেস আর ওয়েব সার্ভার সাধারণত চালু রাখে। সবচেয়ে ভালো উপায়: নিজের কাজ SMT চালু ও বন্ধ দুই অবস্থায় মেপে দেখা।
৭ · OS-এর চোখে hardware thread — logical CPU, sibling আর vCPU
OS-এর কাছে প্রতিটি hardware thread একটি পূর্ণ CPU-র মতো দেখায় — একে বলে logical CPU. ৮টি core, প্রতি core-এ ২টি thread — এমন chip-এ Linux-এর nproc বলবে ১৬, Windows-এর Task Manager দেখাবে “Logical processors: 16”. একই core-এর hardware thread-গুলোকে বলে sibling thread. Linux প্রতিটি logical CPU-র জন্য /sys/devices/system/cpu/cpuN/topology/thread_siblings_list ফাইলে লিখে রাখে কারা তার sibling (যেমন 0,8 বা 0-1 — নম্বর দেওয়ার রীতি chip আর firmware-ভেদে আলাদা)।
Scheduler-এর জন্য এই তথ্য জরুরি। দুটি ব্যস্ত task আর দুটি খালি physical core থাকলে, task দুটিকে একই core-এর দুই sibling-এ বসানো বোকামি — তারা slot আর cache নিয়ে লড়বে, অথচ পাশের core অলস। তাই SMT-সচেতন scheduler আগে আলাদা core-এ ছড়ায়, সব core ব্যস্ত হলে তবেই sibling ভরায়। আবার একই core-এর sibling-এর মধ্যে task সরানো প্রায় বিনামূল্যে, কারণ cache একই। Linux এজন্য scheduling domain-এর স্তর সাজায় — sibling, core, socket, NUMA node. OS-এর দিক থেকে পুরো আলোচনা আছে Operating Systems কোর্সের Multiprocessor scheduling পর্বে (multicore আর SMT অংশ), আর software thread-এর মূল ধারণা থ্রেড পর্বে।
Cloud-এ একটি “vCPU” কেনার অর্থও এখান থেকে বোঝা যায়। বড় cloud সেবাগুলোর অনেক x86 instance-এ একটি vCPU সাধারণত একটি hardware thread — অর্থাৎ ২টি vCPU প্রায়ই একটি physical core-এর দুই sibling. তাই “৪ vCPU” মানে প্রায়ই ২টি পূর্ণ core-এর সমান কাজের ক্ষমতা নয়। সেবাভেদে আর instance-ভেদে নিয়ম আলাদা, তাই নির্দিষ্ট সেবার ডকুমেন্টেশন দেখে নেবেন।
যে runner-এ এই পর্বের কোড চলে, সেটি কী দেখায়? নিচের script শুধু logical CPU-র সংখ্যা আর sibling-এর তালিকা ছাপে — কোনো CPU-র নাম, নির্মাতা, flag বা যন্ত্রের পরিচয় নয় (ফাইল থেকে সংখ্যা, কমা আর ড্যাশ ছাড়া সব বাদ দেওয়া হয়):
# Logical CPUs and SMT sibling lists ONLY -- no model name, vendor, flags or host info
echo "nproc (CPUs this process may use): $(nproc 2>/dev/null || echo '?')"
echo "getconf _NPROCESSORS_ONLN (online logical CPUs): $(getconf _NPROCESSORS_ONLN 2>/dev/null || echo '?')"
lists=""; found=0
for f in /sys/devices/system/cpu/cpu[0-9]*/topology/thread_siblings_list; do
[ -r "$f" ] || continue
c=${f#/sys/devices/system/cpu/}; c=${c%%/*} # e.g. cpu0
s=$(tr -cd '0-9,-' < "$f") # keep only digits, commas, dashes
echo "$c thread_siblings_list: $s"
lists="$lists $s"; found=$((found + 1))
done
if [ "$found" -eq 0 ]; then
echo "thread_siblings_list পড়া যায় না (এই container-এ বন্ধ)"; exit 0
fi
count() { # how many CPUs in a list like 0-1 or 0,4 or 3
local n=0 part a b
for part in ${1//,/ }; do
if [[ $part == *-* ]]; then a=${part%-*}; b=${part#*-}; n=$((n + b - a + 1)); else n=$((n + 1)); fi
done
echo "$n"
}
uniq_lists=$(echo $lists | tr ' ' '\n' | sort -u)
cores=$(echo "$uniq_lists" | wc -l)
per=$(count "$(echo "$uniq_lists" | head -1)")
echo "distinct sibling groups (cores as seen): $cores, hardware threads per group: $per"যাচাইয়ের সময় আউটপুট:
nproc (CPUs this process may use): 2
getconf _NPROCESSORS_ONLN (online logical CPUs): 2
cpu0 thread_siblings_list: 0-1
cpu1 thread_siblings_list: 0-1
distinct sibling groups (cores as seen): 1, hardware threads per group: 2
Runner দেখছে ২টি logical CPU, আর দুটির sibling-তালিকাই 0-1 — অর্থাৎ kernel-এর চোখে এরা একই core-এর দুটি hardware thread, আলাদা দুটি core নয়। কিন্তু এই runner একটি ভাগ করা virtual machine-এর ভেতরে একটি container. এই topology hypervisor-এর দেখানো ছবি — একটি cloud সার্ভারের x86-64 CPU-র দুটি vCPU, যা বাস্তবে একই core-এর sibling-ও হতে পারে, আলাদা core-ও। তাই এই সংখ্যা আসল hardware সম্পর্কে খুব কমই বলে। আপনার নিজের Linux যন্ত্রে একই script (বা lscpu-এর “Thread(s) per core” লাইন) চালালে আসল ছবি পাবেন।
এবার দুটি thread একসাথে চালিয়ে দেখা যাক। নিচের C প্রোগ্রাম একই কাজ দুবার করে: প্রথমে একটি thread দুটি কাজ পরপর, তারপর দুটি POSIX thread একসাথে একটি করে। কাজটি একটি নির্ভরশীল গুণ-যোগের শৃঙ্খল — প্রতিটি ধাপ আগের ফলের জন্য অপেক্ষা করে, তাই একটি thread-এর ভেতরে ILP প্রায় নেই। SMT-র জন্য এটি আদর্শ ধরনের কাজ।
// One thread vs two threads doing independent work: wall-time ratio (varies per run and machine)
#include <pthread.h>
#include <stdio.h>
#include <time.h>
#define N 60000000u
static void *work(void *arg) { // independent integer work, no shared data
unsigned x = *(unsigned *)arg;
for (unsigned i = 0; i < N; i++) x = x * 1103515245u + 12345u;
*(unsigned *)arg = x; // keep the result so the loop is not removed
return NULL;
}
static double now(void) {
struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t);
return t.tv_sec + t.tv_nsec / 1e9;
}
int main(void) {
unsigned a = 1, b = 2;
double t0 = now();
work(&a); work(&b); // one thread does both jobs, one after another
double one = now() - t0;
a = 1; b = 2; t0 = now();
pthread_t ta, tb; // two threads, one job each, at the same time
if (pthread_create(&ta, NULL, work, &a) || pthread_create(&tb, NULL, work, &b)) return 1;
pthread_join(ta, NULL); pthread_join(tb, NULL);
double two = now() - t0;
printf("checksums: %u %u\n", a, b);
printf("1 thread, 2 jobs : %.2f s\n", one);
printf("2 threads, 2 jobs: %.2f s\n", two);
printf("ratio (1 thread / 2 threads): %.2f\n", one / two);
return 0;
}যাচাইয়ের সময় আউটপুট (সময় প্রতিবার বদলায়):
checksums: 2514772225 3490176258
1 thread, 2 jobs : 0.22 s
2 threads, 2 jobs: 0.13 s
ratio (1 thread / 2 threads): 1.67
Checksum দুটি প্রতিবার একই — দুই পথে একই হিসাব হয়েছে। সময়ের অনুপাত ১-এর বেশি মানে দুই thread একসাথে চলায় কাজ দ্রুত শেষ হয়েছে। কিন্তু সংখ্যাটি অস্থির: এই পর্ব লেখার সময় একই runner-এ পরপর চারবার চালিয়ে অনুপাত পাওয়া গেছে ১.১৩, ১.৫৫, ১.৯৫ আর ওপরের ১.৬৭. কারণ ভাগ করা যন্ত্রে অন্যদের কাজও চলে, আর দুটি vCPU আসলে কোথায় বসে আছে তা আমরা জানি না। পরিষ্কার যন্ত্রে কী আশা করা যায়, তা অনুশীলন ৪-এ। নিজের যন্ত্রে taskset দিয়ে দুই thread-কে sibling বা আলাদা core-এ বেঁধে পার্থক্য মাপতে পারেন।
৮ · নিরাপত্তা, GPU আর বড় ছবি
SMT-এর side channel আর প্রতিরক্ষা
SMT-এর সবচেয়ে বড় শক্তি — একই core-এর সব কিছু দুই thread-এর ভাগে — নিরাপত্তার দিক থেকে দুর্বলতা। দুটি sibling যদি আলাদা প্রসেস বা আলাদা virtual machine-এর হয়, তাহলে একজন অন্যজনের কাজের “ছায়া” মাপতে পারে: কোন execution port কতটা ব্যস্ত, L1 cache-এর কোন অংশ সরে গেল, TLB-তে কী আছে। এই ধরনের পরোক্ষ তথ্য-ফাঁসকে বলে side channel. ২০১৮ সালে কয়েকটি গবেষণা দল দেখায়, এমন ভাগ করা resource দিয়ে একই core-এ চলা cryptography কোডের গোপন key-এর আভাস পাওয়া সম্ভব — যেমন execution port-এর প্রতিযোগিতা (PortSmash নামে পরিচিত) আর ভাগ করা TLB (TLBleed). একই বছরের L1TF আর ২০১৯ সালের MDS পরিবারের দুর্বলতায় দেখা গেল, কিছু অবস্থায় sibling thread একে অন্যের L1 বা ভেতরের buffer-এর ডেটার আভাস পেতে পারে। পর্ব ৪১-এ speculative execution-এর আক্রমণ আর প্রতিরক্ষার মূল ধারণা দেখেছেন; এখানে শুধু SMT-এর অংশ।
প্রতিরক্ষার তিনটি স্তর:
- SMT বন্ধ: সবচেয়ে নিশ্চিত, দামও সবচেয়ে বেশি — logical CPU অর্ধেক। OpenBSD ২০১৮ সালে ডিফল্টে Hyper-Threading বন্ধ করে। Linux-এ boot-এর সময়
nosmtবা চালু অবস্থায়/sys/devices/system/cpu/smt/controlদিয়ে বন্ধ করা যায় (kernel-এর version-ভেদে)। - Core scheduling: একই core-এর sibling-এ শুধু পরস্পর-বিশ্বস্ত task (যেমন একই প্রসেস বা একই VM-এর) একসাথে চলবে; বিশ্বস্ত কেউ না থাকলে sibling অলস থাকবে। Linux-এ ২০২১ সালের দিকে (5.14) এটি যোগ হয়েছে। Hypervisor-গুলোও আলাদা গ্রাহকের vCPU একই core-এর দুই sibling-এ না বসানোর নীতি নেয়।
- Constant-time কোড আর hardware সংশোধন: cryptography কোড এমনভাবে লেখা যাতে গোপন ডেটার ওপর নির্ভর করে branch বা মেমরি-ঠিকানা না বদলায়; আর নতুন chip ও microcode কিছু buffer ফাঁস বন্ধ করে।
GPU — হাজার হাজার thread দিয়ে latency লুকানো
CPU মেমরির latency-র বিরুদ্ধে লড়ে মূলত তা কমিয়ে — বড় cache, prefetch, out-of-order. GPU পুরো উল্টো পথ নেয়: latency সহ্য করে, অসংখ্য thread দিয়ে ঢেকে। একটি GPU core (NVIDIA-র ভাষায় streaming multiprocessor) একসাথে বহু thread-দলের (warp) অবস্থা register-এ রেখে দেয়, আর প্রায় প্রতি cycle-এ একটি প্রস্তুত দল বেছে issue করে — একটি দল মেমরির অপেক্ষায় থাকলে অন্যটি চলে। এটি বিশাল মাত্রার fine-grained multithreading; দাম — একক thread খুব ধীর। পর্ব ৫৫-এ GPU-র SIMT মডেল বিস্তারিত।
Multithreading, multicore আর multiprocessor
| কী বাড়ানো হয় | কী ভাগ করা | OS কী দেখে | |
|---|---|---|---|
| Hardware multithreading | এক core-এ বেশি thread-অবস্থা (PC, register) | Execution unit, L1/L2, predictor | প্রতি thread একটি logical CPU |
| Multicore (পর্ব ৫৩) | এক chip-এ বেশি পূর্ণ core | সাধারণত শুধু শেষ স্তরের cache আর মেমরির পথ | প্রতি core-এ এক বা একাধিক logical CPU |
| Multiprocessor (পর্ব ৫৪) | বেশি chip (socket) | মেমরি — প্রায়ই NUMA, interconnect দিয়ে | সব socket-এর সব logical CPU |
আজকের server-এ তিনটিই একসাথে থাকে। পর্ব ৫৩–৫৪-এ cache coherence আর NUMA আসবে — একই core-এর thread-রা একই L1 দেখে, কিন্তু আলাদা core-এ সেটিই কঠিন প্রশ্ন।
৯ · AI-এর যুগে — multithreading-এর প্রশ্নে AI, আর যাচাইয়ের অভ্যাস
Hardware multithreading নিয়ে AI-এর উত্তর প্রায়ই সাবলীল, কিন্তু কয়েকটি ভুল বারবার দেখা যায় — বিশেষ করে শব্দ গুলিয়ে ফেলা আর বানানো সংখ্যা:
| AI-এর সাধারণ ভুল | কীভাবে ধরবেন |
|---|---|
| Software thread (pthread) আর hardware thread এক করে ফেলা; “Hyper-Threading মানে প্রোগ্রাম নিজে থেকে দুই ভাগ হয়” | প্রশ্নে স্পষ্ট লিখুন কোনটি; hardware thread software-কে ভাগ করে না |
| Fine-grained আর SMT গুলিয়ে ফেলা (“fine-grained-এ একই cycle-এ দুই thread”) | একটি ছোট issue-slot ছক নিজে আঁকুন; সংজ্ঞার সাথে মেলান |
| “SMT-তে ৩০% লাভ” ধরনের একটি নির্দিষ্ট সংখ্যা, যেন সবসময় সত্য | লাভ কাজভেদে আলাদা; উৎস চান, নিজের কাজ SMT চালু-বন্ধে মাপুন |
| Chip-এর thread-সংখ্যা বা প্রকাশের সাল বানানো (“অমুক chip-এ SMT4”) | নির্মাতার datasheet বা নিজের যন্ত্রে lscpu, thread_siblings_list দেখুন |
| Side channel-এর প্রশ্নে আক্রমণের কোড দিতে চাওয়া, বা “SMT বন্ধ করলেই সব নিরাপদ” বলা | প্রতিরক্ষার স্তর (SMT বন্ধ, core scheduling, constant-time কোড, microcode) আলাদা করে যাচাই করুন |
AI-কে multithreading নিয়ে প্রশ্ন করার সময় মডেলের অনুমানগুলো স্পষ্ট দিন, আর cycle-ধরে যাচাইযোগ্য উত্তর চান:
AI-এর দেওয়া multithreading-ব্যাখ্যা হাতে পেলে — চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- Fine-grained, coarse-grained আর SMT-এর পার্থক্য? — Fine: প্রতি cycle-এ thread বদল; coarse: লম্বা stall-এ বদল, refill-এর দাম; SMT: একই cycle-এ কয়েকটি thread থেকে issue.
- Vertical আর horizontal waste কী, কোনটি কে কমায়? — পুরো খালি cycle বনাম আধা-খালি cycle; fine ও coarse শুধু vertical কমায়, SMT দুটোই।
- Hardware thread-এ কী replicate করতে হয়? — PC, architectural register বা rename map, কিছু CSR/page-table register; execution unit আর cache ভাগ করা।
- R = ৫০, L = ২০০, S = ১০ হলে miss পুরো ঢাকতে কতগুলো thread? — ১ + ⌈২০০ ÷ ৬০⌉ = ৫; utilisation সর্বোচ্চ ৫০ ÷ ৬০ ≈ ৮৩%.
- SMT কেন একক thread-কে ধীর করতে পারে? — Sibling-এর সাথে slot, execution unit, cache আর সারি ভাগ করতে হয়; throughput বাড়ে, latency নয়।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১CDC 6600-এর peripheral processor থেকে Tera MTA — cache ছাড়াই শুধু thread দিয়ে মেমরির latency লুকানোর স্বপ্ন কেন জন্মেছিল, আর কেন তা মূলধারায় আসেনি?
১৯৬৪ সালের CDC 6600-কে Seymour Cray নকশা করেছিলেন দ্রুততম বৈজ্ঞানিক কম্পিউটার হিসেবে। মূল CPU যাতে I/O-র ধীর কাজে সময় নষ্ট না করে, তাই পাশে দশটি “peripheral processor” ছিল। কিন্তু দশটি আলাদা processor বানানো দামি; তাই একটিমাত্র ভাগ করা হিসাব-যন্ত্র দশটি processor-এর অবস্থা (register, PC) পালা করে নিত — প্রতিটি অল্প সময়ের জন্য। ধীর মেমরি আর I/O-র অপেক্ষা এভাবে অন্যদের পালায় ঢাকা পড়ত। এটিই barrel-ধারণার প্রথম দিকের বিখ্যাত রূপ বলে গণ্য।
১৯৮০-এর দশকে Burton Smith এই ধারণাকে মূল CPU-তে আনলেন — Denelcor HEP-এ, তারপর ১৯৯০-এর দশকে Tera MTA-তে। তাঁর যুক্তি ছিল সরল কিন্তু সাহসী: cache দিয়ে latency কমানোর চেষ্টা জটিল, আর অনিয়মিত মেমরি-নকশার প্রোগ্রামে (graph, sparse matrix, ডেটাবেস) cache প্রায়ই ব্যর্থ। তার বদলে প্রতি processor-এ এত thread রাখা হোক, যাতে মেমরির পুরো latency-র সময় অন্য thread-রা কাজ চালায়। MTA প্রতি processor-এ ১২৮টি thread রাখত বলে উল্লেখ পাওয়া যায়, আর data cache প্রায় ছিলই না। Little-এর সূত্রের ভাষায়: latency × throughput = যতগুলো কাজ একসাথে চলমান থাকতে হবে — latency শত cycle হলে শত thread.
তাহলে মূলধারায় এল না কেন? প্রথমত, এত thread পেতে software-কে বিপুলভাবে সমান্তরাল হতে হয়; সাধারণ অফিস বা ডেস্কটপ প্রোগ্রামে তা ছিল না। দ্বিতীয়ত, একক thread অত্যন্ত ধীর — যে কাজ ভাগ করা যায় না, তাতে যন্ত্রটি হতাশাজনক। তৃতীয়ত, বিশাল register file আর কাস্টম মেমরি-ব্যবস্থার দাম; বাজার ছোট।
কিন্তু ধারণাটি অন্য রূপে ফিরেছে: GPU ঠিক এই দর্শনে চলে, যেখানে কাজ বিপুলভাবে সমান্তরাল (গ্রাফিক্স, AI).
মূল উপলব্ধি: Latency-র বিরুদ্ধে দুটি পথ — কমানো (cache, out-of-order) আর লুকানো (অনেক thread). কোনটি জিতবে, তা নির্ভর করে software কতটা সমান্তরাল তার ওপর।
প্র ০২SMT গবেষণাপত্র থেকে বাজারে আসতে প্রায় সাত বছর লাগল। এই যাত্রায় কোন প্রকৌশল-বাধাগুলো পেরোতে হয়েছিল?
১৯৯৫ সালে Tullsen, Eggers আর Levy-র গবেষণাপত্র simulation দিয়ে দেখিয়েছিল, একটি চওড়া superscalar core-এর slot-এর বড় অংশ খালি যায়, আর একই cycle-এ কয়েকটি thread মেশালে utilisation অনেক বাড়ে। গবেষণাপত্রটির বড় শক্তি ছিল issue-slot diagram — vertical আর horizontal waste-এর ছবি, যা এক নজরে দেখায় কেন শুধু fine-grained বা শুধু superscalar যথেষ্ট নয়। পরের বছর তাঁরা DEC-এর গবেষকদের সাথে দেখালেন, একটি বাস্তবসম্মত out-of-order নকশায় অল্প পরিবর্তনেই SMT সম্ভব — কারণ register renaming আর issue queue আগে থেকেই thread-নিরপেক্ষ।
প্রথম বাধা ছিল নকশার ঝুঁকি। DEC Alpha 21464-এ চারটি thread-এর SMT পরিকল্পনা করেছিল, কিন্তু Compaq-এর অধীনে প্রকল্পটি ২০০১ সালের দিকে বাতিল হয়, আর Alpha-র দল Intel-এ যায় বলে জানা যায়। দ্বিতীয় বাধা ছিল যাচাই (verification): দুই thread-এর প্রতিটি সম্ভাব্য মিশ্রণে exception, interrupt, memory ordering ঠিক থাকছে কি না — পর্ব ৩৮-এর precise exception এখন প্রতি thread-এ আলাদা, অথচ একই pipeline-এ। তৃতীয় বাধা ন্যায্যতা: একটি thread যেন অন্যটিকে না খাইয়ে রাখে — যা থেকে ICOUNT-এর মতো fetch নীতি আর সারির ভাগাভাগির নিয়ম এল।
Intel ২০০২ সালের দিকে Xeon-এ আর একই বছরের শেষে Pentium 4-এ Hyper-Threading আনে — দুটি thread, আর Intel-এর প্রকাশিত বর্ণনা অনুযায়ী কয়েকটি সারি দুই ভাগে স্থিরভাবে ভাগ করা, যাতে যাচাই সহজ হয় আর কেউ কাউকে আটকে না রাখে। প্রথম দিকে কিছু কাজে উল্টো ধীর হওয়ার অভিযোগও ছিল — দুই thread একই ছোট cache-এ লড়লে, বা SMT-অসচেতন OS দুটি ব্যস্ত কাজকে একই core-এ বসালে। Windows আর Linux-এর scheduler-কে তাই SMT-সচেতন হতে হয়েছিল। IBM ২০০৪ সালের দিকে POWER5-এ dynamic sharing-নির্ভর SMT আনে আর পরে thread-সংখ্যা চার আর আটে নিয়ে যায়।
মূল উপলব্ধি: একটি ভালো hardware ধারণা বাজারে আসে তখনই, যখন তার যাচাই, ন্যায্যতা আর software-এর সমর্থন — তিনটিই প্রস্তুত।
প্র ০৩GPU আর CPU দুজনেই multithreading ব্যবহার করে, তবু তাদের দর্শন উল্টো। কেন একটির ২টি thread, আরেকটির হাজার?
একটি আধুনিক CPU core-এর লক্ষ্য একক thread-কে যত দ্রুত সম্ভব চালানো। এজন্য সে latency কমায়: বড় বহুস্তর cache, prefetcher, বিশাল out-of-order জানালা, নিখুঁত branch predictor. এত কিছুর পরেও যে ফাঁক থাকে, সেটুকু ঢাকতে ২টি (কিছু server chip-এ ৪ বা ৮টি) hardware thread যথেষ্ট — কারণ বেশির ভাগ latency ইতিমধ্যে কমানো। আর বেশি thread দিলে প্রতিটির জন্য cache-এর ভাগ কমবে, একক thread-এর গতি পড়বে — যা CPU-র মূল প্রতিশ্রুতির বিরুদ্ধে।
GPU-র লক্ষ্য মোট throughput. গ্রাফিক্সে লাখ লাখ pixel, AI-এ কোটি কোটি গুণ — সব পরস্পর-স্বাধীন। এখানে একটি pixel কত দ্রুত শেষ হলো, তা অপ্রাসঙ্গিক; প্রতি সেকেন্ডে কত pixel, সেটাই প্রশ্ন। তাই GPU latency সহ্য করে। একটি GPU core একসাথে বহু thread-দল register-এ বসিয়ে রাখে, আর প্রতি cycle-এ একটি প্রস্তুত দল বেছে নেয়। একটি দল কয়েকশো cycle মেমরির অপেক্ষায় থাকলে বাকিরা চলে। এটি আসলে fine-grained multithreading — কোনো কোনো নকশায় একাধিক দল একসাথে issue করে, যা SMT-র কাছাকাছি।
এর খরচ কোথায় যায়? Out-of-order যন্ত্র আর বিশাল cache-এর জায়গায় GPU রাখে বিপুল register file আর অনেক সরল ALU. প্রতিটি thread-এর register chip-এ বসে থাকে, তাই thread বদলের দাম শূন্য; কিন্তু একক thread অত্যন্ত ধীর।
Little-এর সূত্র দুটিকে এক সূত্রে বাঁধে: একসাথে চলমান কাজ = latency × throughput. CPU latency ছোট করে, তাই অল্প চলমান কাজেই চলে; GPU latency মেনে নেয়, তাই চলমান কাজ বিপুল রাখে। পর্ব ৫৫-এ GPU-র SIMT মডেল আর “occupancy” (কতগুলো দল একসাথে বসানো গেল) দেখবেন — যা আসলে এই পর্বের N*-এর GPU-রূপ।
মূল উপলব্ধি: Thread-সংখ্যা নির্ভর করে কতটা latency লুকাতে হবে আর একক thread-এর গতির দাম কত — CPU আর GPU একই সমীকরণের দুই প্রান্ত।
প্র ০৪২০১৮ সালের পর কিছু OS ডিফল্টে SMT বন্ধ করল। ভাগ করা core কেন নিরাপত্তার ঝুঁকি, আর বন্ধ না করেও ঝুঁকি কমানোর উপায় কী?
Process isolation-এর প্রতিশ্রুতি হলো, একটি প্রসেস অন্যটির মেমরি পড়তে পারবে না। Page table আর privilege level সেই সরাসরি পথ বন্ধ করে। কিন্তু দুটি প্রসেস যখন একই core-এর দুই sibling-এ চলে, তারা একই execution port, একই L1 cache, একই TLB আর ভেতরের কিছু buffer ভাগ করে। একজনের কাজ অন্যজনের সময়ে ছাপ ফেলে — “আমার এই instruction আজ ধীর হলো কেন? কারণ sibling একই port ব্যবহার করছে।” গোপন key-এর বিটের ওপর নির্ভর করে যদি কোডের আচরণ বদলায়, তবে সেই ছাপ থেকে key-এর আভাস মেলে। একে side channel বলে, কারণ তথ্য আসে নিয়মের দরজা দিয়ে নয়, পাশের পথে।
২০১৮ সালে কয়েকটি স্বাধীন গবেষণা এটি হাতে-কলমে দেখায়: PortSmash নামের কাজে execution port-এর প্রতিযোগিতা, TLBleed নামের কাজে ভাগ করা TLB. একই বছর L1TF (Foreshadow) আর ২০১৯ সালে MDS পরিবারের দুর্বলতা দেখাল, কিছু Intel chip-এ speculative execution-এর কারণে sibling thread একে অন্যের L1 বা fill buffer-এর ডেটার আভাস পেতে পারে। এগুলোর অনেকটা এক প্রসেস থেকে অন্য প্রসেসে যাওয়ার সময় buffer মুছে ফেলে ঠেকানো যায় — কিন্তু SMT-তে দুজন একই মুহূর্তে চলছে, মোছার সুযোগই নেই। তাই OpenBSD ২০১৮ সালে ডিফল্টে Hyper-Threading বন্ধ করে, আর কিছু cloud ও hypervisor আলাদা গ্রাহকের vCPU একই core-এ না বসানোর নীতি নেয়।
SMT বন্ধ করা মানে logical CPU অর্ধেক — অনেক কাজে বড় দাম। মাঝামাঝি পথ হলো core scheduling: OS প্রতিটি task-কে একটি “বিশ্বাসের দল”-এ রাখে, আর একই core-এর দুই sibling-এ শুধু একই দলের task একসাথে চলে; মিল না পেলে একটি sibling অলস থাকে। Linux-এ এটি ২০২১ সালের দিকে যোগ হয়। পাশাপাশি cryptography লাইব্রেরি constant-time কোড লেখে, যাতে গোপন ডেটা অনুযায়ী branch বা মেমরি-ঠিকানা না বদলায় — তখন port বা cache-এর ছাপে কিছুই ফাঁস হয় না। আর নতুন chip-এ hardware সংশোধন কিছু buffer-ফাঁস বন্ধ করেছে।
মূল উপলব্ধি: যে resource ভাগ করা, তা তথ্যও ভাগ করতে পারে; SMT-র নিরাপত্তা মানে ঠিক করা কে কার সাথে একই core-এ বসবে।
প্র ০৫অনেক ফোনের core আর কিছু নতুন ল্যাপটপ chip SMT ছাড়াই আসে। SMT কি পুরোনো হয়ে যাচ্ছে? Cloud-এর vCPU-র দামের সাথে এর কী সম্পর্ক?
SMT-র যুক্তি ছিল: একটি বড়, দামি out-of-order core-এর খালি সময় অল্প বাড়তি খরচে কাজে লাগানো। কিন্তু এই যুক্তির প্রতিটি অংশ এখন প্রশ্নের মুখে। প্রথমত, শক্তি। ফোনে আর ল্যাপটপে মূল মাপকাঠি প্রায়ই performance per watt. দুটি thread একই core-এ চালালে core বেশি সময় পূর্ণ শক্তিতে জ্বলে; অনেক ক্ষেত্রে একই কাজ একটি ছোট, শক্তি-সাশ্রয়ী core-এ সরিয়ে দেওয়া বেশি কার্যকর। Apple-এর M-series আর অনেক ARM নকশা এই পথ নিয়েছে: SMT নয়, বড় আর ছোট core-এর মিশ্রণ (big.LITTLE, P-core আর E-core).
দ্বিতীয়ত, নিরাপত্তা আর যাচাইয়ের খরচ — প্র ০৪-এর সব প্রশ্ন SMT ছাড়া উঠতই না। তৃতীয়ত, scheduler-এর জটিলতা: বড়-ছোট core আর SMT একসাথে থাকলে OS-কে বুঝতে হয়, একটি কাজ কোনটিতে রাখলে ভালো — একটি বড় core-এর দ্বিতীয় sibling, না একটি ছোট core. এসব কারণেই ২০২৪ সালের কিছু Intel ল্যাপটপ chip-এর বড় core থেকে Hyper-Threading বাদ দেওয়া হয়েছে বলে খবর; Intel-এর ব্যাখ্যা মূলত শক্তি-দক্ষতা আর আয়তনের হিসাব বলে জানা যায়।
অন্যদিকে server-এ SMT এখনো শক্ত অবস্থানে: ডেটাবেস, ওয়েব আর অনেক-অনুরোধের কাজে throughput-ই মাপকাঠি, আর মেমরির অপেক্ষা প্রচুর। AMD, Intel-এর server chip আর IBM POWER এখনো SMT রাখে। এখানে cloud-এর অর্থনীতি আসে। অনেক cloud সেবায় একটি vCPU মানে একটি hardware thread, তাই একটি physical core বিক্রি হয় দুটি vCPU হিসেবে। তাই ৮ vCPU-র একটি VM সম্ভবত ৪টি পূর্ণ core-এর মতো; ALU-ভরা হিসাবে ৮টি thread চালিয়ে ৮ গুণ গতি পাবেন না।
মূল উপলব্ধি: SMT একটি trade-off, স্থায়ী উন্নতি নয় — কাজের ধরন, শক্তির বাজেট আর নিরাপত্তার চাহিদা বদলালে উত্তরও বদলায়।
অনুশীলন
পরীক্ষার ধাঁচে: coarse-grained-এর utilisation, fine-grained-এ কে issue করে, SMT-এর issue-slot ছক ভরানো আর আউটপুট অনুমান। আগে নিজে চেষ্টা করুন।
-
Coarse-grained-এর হিসাব: একটি core-এ প্রতিটি thread গড়ে R = ৪০ cycle চলে, তারপর একটি cache miss-এ L = ১২০ cycle অপেক্ষা; thread বদলের দাম S = ৮ cycle. (ক) ১, ২ আর ৪টি thread-এ utilisation কত? (খ) Miss পুরো ঢাকতে সবচেয়ে কম কতগুলো thread লাগবে, আর তখন utilisation কত? (গ) Pipeline গভীর হওয়ায় S = ১৩০ হলে ২টি thread-এ utilisation কত — বদল কি লাভজনক?
(ক) এক thread: ৪০ ÷ (৪০ + ১২০) = ২৫%. দুটি: চক্র P = max(২ × ৪৮, ৪০ + ১২০ + ৮) = max(৯৬, ১৬৮) = ১৬৮, তাই ৮০ ÷ ১৬৮ ≈ ৪৭.৬%. চারটি: P = max(৪ × ৪৮, ১৬৮) = max(১৯২, ১৬৮) = ১৯২, তাই ১৬০ ÷ ১৯২ ≈ ৮৩.৩%.
(খ) N* = ১ + ⌈১২০ ÷ ৪৮⌉ = ১ + ⌈২.৫⌉ = ১ + ৩ = ৪. তখন utilisation = R ÷ (R + S) = ৪০ ÷ ৪৮ ≈ ৮৩.৩% — (ক)-এর চার-thread উত্তরের সাথে মিলে গেল। তিনটি thread-এ: P = max(১৪৪, ১৬৮) = ১৬৮, ১২০ ÷ ১৬৮ ≈ ৭১.৪% — এখনো অপেক্ষা পুরো ঢাকেনি।
(গ) P = max(২ × ১৭০, ৪০ + ১২০ + ১৩০) = max(৩৪০, ২৯০) = ৩৪০, তাই ৮০ ÷ ৩৪০ ≈ ২৩.৫% — এক thread-এর ২৫%-এর চেয়েও কম। কারণ S = ১৩০ > L = ১২০: miss-এর অপেক্ষার চেয়ে বদলই বেশি দামি। নিয়ম মিলে গেল — বদল লাভজনক শুধু S < L হলে। (সংখ্যাগুলো অংশ ৩-এর calculator-এর একই সূত্রে মিলিয়ে দেখা হয়েছে।)
-
Fine-grained-এ কে issue করে (হাতে trace): একটি single-issue (১-wide) fine-grained core-এ তিনটি thread A, B, C, ক্রম A → B → C → A … প্রতিটি thread প্রতি পালায় ১টি instruction issue করে। A-র দ্বিতীয় instruction আর B-র চতুর্থ instruction load, যা miss করে; miss latency ৫ cycle (cycle t-তে issue হলে ওই thread আবার issue করতে পারে cycle t + ৫ থেকে)। C কখনো miss করে না। (ক) নিয়ম “অপেক্ষমাণ thread বাদ, ক্রমে পরের প্রস্তুত thread” হলে cycle ০ থেকে ৯-এ কে issue করে? (খ) কঠোর barrel হলে (অপেক্ষমাণ thread-এর পালা খালি যায়) একই ১০ cycle-এ? দুই ক্ষেত্রে কতগুলো instruction?
(ক) c0 A, c1 B, c2 C, c3 A (এটি A-র দ্বিতীয় — load miss; A আবার প্রস্তুত cycle ৮-এ), c4 B, c5 C, c6 — পালা A-র, কিন্তু A অপেক্ষায়, তাই B (B-র তৃতীয়), c7 C, c8 A (প্রস্তুত), c9 B (B-র চতুর্থ — load miss). ক্রম: A B C A B C B C A B — ১০ cycle-এ ১০টি instruction, একটি cycle-ও খালি নয়।
(খ) কঠোর barrel-এ পালা বদলায় না: c0 A, c1 B, c2 C, c3 A (miss), c4 B, c5 C, c6 — A-র পালা, A অপেক্ষায়: খালি, c7 B, c8 C, c9 A (cycle ৮ থেকে প্রস্তুত)। ক্রম: A B C A B C · B C A — ৯টি instruction. একটি thread বাদ দেওয়ার যুক্তি (skip) একটি slot বাঁচাল; দাম — issue-এর যুক্তি একটু জটিল, আর barrel-এর “একই thread-এর দুটি instruction কখনো কাছাকাছি নয়” নিশ্চয়তা ভেঙে যায় (c6-এ B চলল c4-এর মাত্র ২ cycle পরে)। এই trace একটি ছোট simulator দিয়ে মিলিয়ে দেখা হয়েছে।
-
SMT-এর issue-slot ছক ভরান: ৪-wide SMT core, দুটি thread. A-র দল: ২, ৩, ১ (load miss), ২. B-র দল: ৩, ১ (load miss), ২, ২. একটি দলের instruction একসাথে issue হতে পারে (slot কম পড়লে বাকিগুলো পরের cycle-এ); পরের দল পরের cycle-এর আগে নয়। Miss latency ৩ (cycle t-তে load issue হলে নির্ভরশীল দল cycle t + ৩ থেকে)। অগ্রাধিকার: জোড় cycle-এ আগে A, বিজোড় cycle-এ আগে B. Cycle ০ থেকে ৫-এর ছক ভরান; মোট utilisation আর প্রতি thread-এর instruction-সংখ্যা কত? কোন cycle-গুলো vertical waste?
c0 (আগে A): A-র ২টি, তারপর B-র দলের ৩টির ২টি (slot ২টিই বাকি) →
A A B B; B-র দলে ১টি বাকি। c1 (আগে B): B-র বাকি ১টি, তারপর A-র দ্বিতীয় দলের ৩টি →B A A A. c2 (আগে A): A-র load (১টি, miss → A আবার cycle ৫-এ), B-র load (১টি, miss → B আবার cycle ৫-এ) →A B . .. c3 আর c4: দুজনেই অপেক্ষায় →. . . .. c5 (আগে B): B-র ২টি, A-র ২টি →B B A A.cycle 0 1 2 3 4 5 slot0 A B A . . B slot1 A A B . . B slot2 B A . . . A slot3 B A . . . AA: ২ + ৩ + ১ + ২ = ৮টি, B: ৩ + ১ + ২ = ৬টি, মোট ১৪টি; utilisation ১৪ ÷ (৪ × ৬) = ১৪ ÷ ২৪ ≈ ৫৮.৩%. Vertical waste: cycle ৩ আর ৪ (দুটি miss একসাথে চলছে); horizontal waste: cycle ২-এর ২টি slot. এই ছক অংশ ৬-এর simulator-এর একই নিয়মে চালিয়ে মিলিয়ে দেখা হয়েছে।
-
আউটপুট অনুমান: অংশ ৭-এর
two_threads.c(দুটি স্বাধীন, নির্ভরশীল গুণ-যোগের শৃঙ্খল; একটি thread-এ পরপর বনাম দুটি thread-এ একসাথে) তিনটি পরিষ্কার, অলস যন্ত্রে চালানো হলো: (ক) ১টি core, SMT নেই; (খ) ২টি core, SMT নেই; (গ) ১টি core, ২-way SMT. প্রতিটিতে শেষ লাইনেরratioমোটামুটি কত আশা করবেন, আর কেন? (ঘ) কাজটি যদি নির্ভরশীল শৃঙ্খল না হয়ে প্রতি cycle-এ ৪টি স্বাধীন গুণ চালাত (multiplier আগে থেকেই পুরো ব্যস্ত), তাহলে (গ)-এর উত্তর কীভাবে বদলাত?(ক) প্রায় ১.০০ (সামান্য কমও হতে পারে): একটিই hardware thread, তাই OS দুই software thread-কে পালা করে চালায়; মোট কাজ একই, সাথে context switch-এর সামান্য খরচ। (খ) প্রায় ২.০০: দুটি পূর্ণ core, কোনো ভাগাভাগি নেই, কাজ স্বাধীন। (গ) ১ আর ২-এর মাঝে, এই কাজে সম্ভবত ২-এর দিকে: শৃঙ্খলের প্রতিটি গুণ আগেরটির ফলের অপেক্ষায়, তাই একটি thread multiplier-কে প্রতি cycle ব্যস্ত রাখতে পারে না — খালি cycle-গুলো sibling নেয়। ঠিক কত, তা core-এর multiplier-এর latency আর সংখ্যার ওপর নির্ভর করে; মেপেই জানতে হবে।
(ঘ) তখন একটি thread একাই multiplier ভরিয়ে রাখত; দ্বিতীয় thread-এর জন্য খালি slot প্রায় নেই। (গ)-এর অনুপাত নেমে ১-এর কাছে আসত — SMT-র “দুজনেই একই unit চায়” ক্ষেত্র। (খ) তখনো প্রায় ২ থাকত, কারণ দুটি core-এর multiplier আলাদা। এই পার্থক্যই দেখায়, SMT থেকে কতটা লাভ হবে তা কাজের ধরনের ওপর নির্ভর করে, আর একটি vCPU একটি পূর্ণ core নয়। অংশ ৭-এর runner-এ পাওয়া ১.১৩ থেকে ১.৯৫-এর ওঠানামা এই তিন আদর্শ অবস্থার কোনোটির সাথে নিশ্চিতভাবে মেলানো যায় না — ভাগ করা যন্ত্রে মাপের এটাই সীমা।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমCoarse-grained multithreading-এ একটি thread বদলের ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- ▶️ Thread A চলছে, B একটি hardware thread-এ প্রস্তুত বসে আছে
- 🐢 A-র একটি load last-level cache-এ miss করে, ডেটা আনতে শত cycle লাগবে
- 🔀 Core লম্বা stall চিনে B-তে বদলের সিদ্ধান্ত নেয়, A-র পরের instruction flush হয়
- ⏳ B-র instruction fetch থেকে issue পর্যন্ত আসে — switch penalty-র খালি cycle
- 🏃 B চলে, পেছনে A-র miss মেমরি থেকে ডেটা আনে
- ↩️ B কোনো লম্বা stall-এ থামলে, আর A-র ডেটা এসে গেলে, core আবার A-তে ফেরে
একটি ৪-wide superscalar core-এ একমাত্র thread একটি cache miss-এর অপেক্ষায়, তাই একটি cycle-এ ৪টি issue slot-এর একটিও ভরল না। Tullsen-দের issue-slot পরিভাষায় এটি কী?
- Horizontal waste
- Vertical waste
- Structural hazard
- Switch penalty
নিচের কোন কৌশল একই cycle-এর ভেতরের খালি slot (horizontal waste) কমাতে পারে?
- Coarse-grained multithreading
- Fine-grained multithreading
- Barrel processor-এর কঠোর round-robin
- Simultaneous multithreading (SMT)
একটি coarse-grained core-এ প্রতিটি thread R = ৩০ cycle চলে, তারপর miss-এ L = ৯০ cycle অপেক্ষা; বদলের দাম S = ১০ cycle; thread ২টি। সূত্র U = N·R ÷ max(N(R + S), R + L + S) ধরে utilisation কত?
- ২৫%
- প্রায় ৪৬.২%
- ৭৫%
- ৬০%
একটি সাধারণ ২-way SMT out-of-order core-এ নিচের কোনটি সাধারণত প্রতিটি hardware thread-এর জন্য আলাদা রাখা হয় না, বরং দুজনের ভাগে থাকে?
- Program counter (PC)
- Architectural register-এর অবস্থা (rename map)
- Integer ALU আর অন্যান্য execution unit
- Page table-এর ঠিকানা রাখার register
একটি fine-grained core তিনটি thread A, B, C-কে ক্রমে A → B → C পালা দেয়, আর নিয়ম হলো অপেক্ষমাণ thread বাদ দিয়ে ক্রমে পরের প্রস্তুত thread. এক cycle-এ পালা A-র, কিন্তু A একটি cache miss-এর অপেক্ষায়; B আর C দুজনেই প্রস্তুত। ওই cycle-এ কে issue করবে?
- B
- C
- কেউ না — A-র পালা খালি যায়
- B আর C দুজনেই
একটি chip-এ ৮টি physical core, প্রতি core-এ ২টি hardware thread (২-way SMT), আর সব চালু। Linux-এ nproc কত দেখাবে, আর cpu0-এর thread_siblings_list-এ কয়টি CPU থাকবে?
- ৮; ১টি
- ১৬; ১টি
- ১৬; ২টি
- ৩২; ৪টি
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Vertical waste = পুরো খালি cycle (লম্বা stall), horizontal waste = আধা-খালি cycle (ILP-র অভাব); utilisation = issue হওয়া instruction ÷ (W × cycle).
- Hardware thread = core-এ একটি thread-এর PC, register বা rename map আর কিছু CSR-এর ঘর; execution unit, cache, predictor ভাগ করা — দ্বিতীয় core নয়।
- Coarse-grained: লম্বা stall-এ বদল, দাম S; UN = N·R ÷ max(N(R + S), R + L + S), N* = ১ + ⌈L ÷ (R + S)⌉, লাভ শুধু S < L হলে।
- Fine-grained প্রতি cycle-এ পালা করে — vertical waste মোছে, একক thread ধীর; SMT একই cycle-এ কয়েকটি thread — দুই ধরনের waste-ই কমায়; লাভ কাজভেদে খুব আলাদা।
- OS-এর কাছে প্রতিটি hardware thread একটি logical CPU; sibling-রা resource ভাগ করে, তাই scheduler সচেতন হয় আর নিরাপত্তায় SMT বন্ধ বা core scheduling লাগে; GPU একই ধারণার চরম রূপ।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪৪ · মেমরি হায়ারার্কি আর locality — কেন cache কাজ করেপরবর্তী পর্বযে latency লুকাতে multithreading, তার উৎস।
- কম্পিউটার আর্কিটেকচার ও ডিজিটাল লজিক · কোর্স হোম৬২টি পর্বপুরো কোর্সের রোডম্যাপ।
- Operating Systems · Multiprocessor আর real-time schedulingপাশের ট্র্যাকSMT-সচেতন scheduler, affinity আর load balancing.
- Operating Systems · থ্রেড — কেন, কীভাবে আর threading modelপাশের ট্র্যাকSoftware thread-এর দিক: user আর kernel thread.
- Operating Systems · POSIX thread হাতে-কলমেহাতে-কলমেpthread_create, join আর thread pool.