Foundationপ্রথম নীতি থেকে
LEVEL 11লেসন ৩/১৭অ্যাডভান্সড১ ঘণ্টা ১৫ মিনিট

Cache Coherence ও MESI Protocol — সবার cache একমত থাকার নিয়ম

Cache Coherence and the MESI Protocol

একাধিক core-এর প্রতিটার নিজস্ব private cache থাকায় একই memory address-এর ভিন্ন কপি ভিন্ন core-এ একসাথে থাকতে পারে — cache coherence protocol (MESI: Modified/Exclusive/Shared/Invalid) সেই কপিগুলোকে bus/interconnect message দিয়ে সমন্বিত রাখে, প্রতিটা লেখাকে অন্য সব কপি invalidate করতে বাধ্য করে — আর এই সমন্বয়ের real, non-zero খরচই পরের লেসনের false sharing সমস্যার মূল কারণ।

এই লেসন শেষে আপনি পারবেন

  • Cache coherence সমস্যাটা ঠিক কেন multi-core-এ জন্ম নেয় তা private per-core cache-এর প্রেক্ষাপটে ব্যাখ্যা করতে পারবেন
  • MESI-এর চারটা state (Modified, Exclusive, Shared, Invalid)-এর সংজ্ঞা ও অর্থ নির্ভুলভাবে বলতে পারবেন
  • দুইটা core-এর read-then-write একটা সম্পূর্ণ state-transition sequence হাতে trace করতে পারবেন, প্রতিটা ধাপে inter-core message-সহ
  • MOESI-এর Owned state ঠিক কোন সমস্যা সমাধান করে তা MESI-এর সাথে তুলনা করে ব্যাখ্যা করতে পারবেন
  • Coherence traffic কেন 'বিনামূল্যে' না — বাস্তব latency ও bandwidth খরচ concrete উদাহরণে দেখাতে পারবেন
  • Cache coherence আর memory consistency-র মধ্যে পার্থক্য চিহ্নিত করতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

আগের লেসনে (cache-hierarchy-inclusive-exclusive) আমরা একটা গুরুত্বপূর্ণ বাস্তবতা প্রতিষ্ঠা করেছি: একটা আধুনিক multi-core চিপে L1 আর L2 সাধারণত private — প্রতিটা core-এর নিজস্ব, শুধু তারই ব্যবহারের জন্য। এই একটা তথ্যই একটা গভীর সমস্যার জন্ম দেয়, যা single-core CPU-তে কখনো ছিলই না।

কল্পনা করুন দুইটা core, A আর B, একই memory address X শেয়ার করছে (হয়তো একটা shared variable, দুইটা thread-এর মধ্যে)। Core A ঠিকানা X পড়ে, তার নিজের private L1-এ একটা কপি রেখে দেয়। এখন Core B-ও X পড়ে, তারও নিজের private L1-এ আরেকটা, আলাদা কপি তৈরি হয়। এই মুহূর্তে memory-তে X-এর মান, আর দুইটা cache-এ X-এর কপি — তিনটাই আলাদা জায়গায়, আলাদা হার্ডওয়্যারে বসে আছে, একই মান নিয়ে (আপাতত)।

এখন সমস্যাটা কল্পনা করুন: Core A যদি X-এ লেখে, তার নিজের L1-এর কপি বদলে যায় — কিন্তু Core B-র L1-এর কপি? সেটা তো A-র write সম্পর্কে কিছুই জানে না! B যদি এরপর X পড়ে, সে তার নিজের পুরনো (stale) কপি থেকে পড়বে — একটা ভুল, বাসি মান, অথচ B-র hardware-এর দৃষ্টিতে এটা একটা সম্পূর্ণ স্বাভাবিক cache hit।

এটাই cache coherence problem — single-core-এ caching ছিল সম্পূর্ণ অদৃশ্য একটা optimization (memory-hierarchy লেসনের মূল প্রতিশ্রুতি — cache থাকুক বা না থাকুক, প্রোগ্রামের ফলাফল একই, শুধু গতি ভিন্ন)। Multi-core-এ, private cache-এর উপস্থিতি হঠাৎ একটা দৃশ্যমান, সঠিকতা-সম্পর্কিত সমস্যা হয়ে ওঠে যদি hardware কিছু না করে। Cache coherence protocol হলো সেই hardware সমাধান — একটা নিয়মের সেট, যা নিশ্চিত করে সব core একই memory address সম্পর্কে সবসময় একটা সামঞ্জস্যপূর্ণ ছবি দেখে, private cache থাকা সত্ত্বেও। আজকের লেসনে আমরা সবচেয়ে বহুল-ব্যবহৃত এমন protocol — MESI — সম্পূর্ণভাবে দেখব।

মূল ধারণা

Cache Coherence-এর সংজ্ঞা — দুইটা গ্যারান্টি

একটা memory system-কে “coherent” বলা হয় যদি এটা দুইটা গ্যারান্টি দেয়:

১. Write propagation — একটা core-এর লেখা পরিবর্তন শেষ পর্যন্ত অন্য সব core-এর কাছে দৃশ্যমান হবে (চিরকাল লুকানো থাকবে না)।

২. Write serialization (write serialization/ single writer-multiple reader) — একই address-এ একাধিক লেখা সব core একই ক্রমে দেখবে — কোনো দুইটা core ভিন্ন ক্রমে একই দুইটা লেখা “দেখবে” না।

MESI — চারটা State

MESI নামটাই তার চারটা state-এর আদ্যক্ষর:

Stateপূর্ণ নামঅর্থ
MModifiedএই cache-এই একমাত্র কপি, dirty (memory-র মানের চেয়ে ভিন্ন — write হয়েছে, memory-তে এখনো পৌঁছায়নি)
EExclusiveএই cache-এই একমাত্র কপি, কিন্তু clean (memory-র মানের সাথে অভিন্ন)
SSharedএকাধিক cache-এ একই সাথে থাকতে পারে এমন কপি, সবগুলোই clean, memory-র সাথে অভিন্ন
IInvalidএই cache-এর কপি stale/অকেজো — hardware এটাকে ব্যবহারযোগ্য বলে গণ্য করে না, hit হিসেবে ধরবে না
                    এই cache-ই একমাত্র?          একাধিক cache-এ থাকতে পারে?
                  ┌───────────────────────┐   ┌───────────────────────┐
   dirty (memory   │                       │   │                       │
   থেকে ভিন্ন)      │      M (Modified)      │   │     (এই কম্বিনেশন       │
                    │                       │   │      MESI-তে নেই)      │
                  ├───────────────────────┤   ├───────────────────────┤
   clean (memory    │                       │   │                       │
   সাথে অভিন্ন)      │      E (Exclusive)     │   │      S (Shared)        │
                  └───────────────────────┘   └───────────────────────┘

                              I (Invalid) — কপি অকেজো, উপরের কোনো বিভাগেই নেই
MESI-এর চারটা state — কে একা, কে দলে, কে dirty, কে clean।
                         ┌─────────────────────────────────────┐
                         │                                       │
                         ▼                                       │
        ┌──────┐   L-read, কেউ সাড়া দেয়নি    ┌──────┐         │
        │  I   │ ─────────────────────────────► │  E   │         │
        └──┬───┘                                └──┬───┘         │
           │ L-read, কেউ সাড়া দিয়েছে                │ L-write (bus message ছাড়াই)
           │                                        ▼             │
           │                                     ┌──────┐         │
           └───────────────────────────────────► │  M   │◄────────┘
                                                  └──┬───┘   R-write (data+writeback), M→I
           ┌──────┐  L-write (invalidate broadcast)  │
           │  S   │ ──────────────────────────────────┘
           └──┬───┘
              │ R-write (invalidate নিজেকে)
              ▼
           ┌──────┐
           │  I   │
           └──────┘

R-read একটা M বা E কপিকে S-এ নামায় (data সরবরাহসহ); S কপি R-read-এ S-ই থাকে (নতুন sharer যোগ হয়)।
MESI-এর সব সম্ভাব্য state-transition — একসাথে, সংক্ষেপে। L=local access, R=remote snoop।

ভেতরে কী ঘটছে

কীভাবে state বদলায় — bus snooping ও inter-core message

MESI বাস্তবায়ন করতে প্রতিটা core-এর cache controller-কে অন্য সব core-এর memory transaction “শুনতে” হয় — একে বলে bus snooping। যখন কোনো core একটা address-এ read বা write করতে চায়, সে একটা বার্তা bus/interconnect-এ পাঠায়, আর প্রতিটা অন্য cache controller সেই বার্তা দেখে নিজের copy-র state যাচাই ও প্রয়োজনে আপডেট করে।

সাধারণ (textbook-এ প্রচলিত নামে) বার্তার ধরন:

  • BusRd — “আমি এই address পড়তে চাই” (একটা read miss-এর ফলে পাঠানো হয়)
  • BusRdX (বা Read-For-Ownership/RFO) — “আমি এই address-এ লিখতে চাই, এবং এই মুহূর্তে আমার কাছে বৈধ ডেটা নেই” — ডেটা আনা এবং অন্য সবাইকে invalidate করা, একসাথে
  • BusUpgr (বা Invalidate) — “আমার কাছে ইতিমধ্যে বৈধ (S state) ডেটা আছে, আমি শুধু লেখার অনুমতি (exclusive ownership) চাই — ডেটা পাঠানোর দরকার নেই, শুধু অন্য সবাইকে invalidate করো”

Broadcast bus বনাম directory — স্কেলের সমস্যা

উপরের সবকিছু ধরে নিচ্ছে একটা shared bus আছে, যেখানে সব core-এর সব snoop message broadcast হয় — প্রতিটা core প্রতিটা অন্য core-এর প্রতিটা memory transaction দেখে। এটা ছোট core-সংখ্যায় (৪-৮টা) ব্যবহারিক, কিন্তু বড় সংখ্যায় (৩০+) broadcast ট্র্যাফিক নিজেই একটা বাধা হয়ে ওঠে।

আগের লেসনের (cache-hierarchy-inclusive-exclusive) hood অংশে ঠিক এই সমস্যার একটা সমাধান দেখানো হয়েছিল — একটা inclusive shared L3, presence bits-সহ, broadcast এড়িয়ে শুধু প্রাসঙ্গিক core-দের targeted invalidate message পাঠাতে পারে। এটাকে আরও সাধারণীকরণ করলে পাওয়া যায় directory-based coherence — L3 (বা একটা আলাদা structure) প্রতিটা line-এর জন্য একটা “directory entry” রাখে (কোন core-দের কাছে কপি আছে), আর coherence action সরাসরি সেই directory দেখে ঠিক কাকে বার্তা পাঠাতে হবে তা জানে, বাকিদের বিরক্ত না করেই।

broadcast snooping: O(N) বার্তা প্রতি coherence action, N=core সংখ্যা\text{broadcast snooping: } O(N) \text{ বার্তা প্রতি coherence action, } N = \text{core সংখ্যা} directory-based: O(actual sharer সংখ্যা) বার্তা, প্রায়ই N থেকে অনেক কম\text{directory-based: } O(\text{actual sharer সংখ্যা}) \text{ বার্তা, প্রায়ই } N \text{ থেকে অনেক কম}

এই কারণেই বড় multi-core/multi-socket সিস্টেম (৩০+ core, একাধিক socket) প্রায় সবসময় directory-based coherence ব্যবহার করে, ছোট desktop/laptop-grade চিপ প্রায়ই সরল broadcast snooping-এই যথেষ্ট পায়।

একটা সম্পূর্ণ state-transition রেফারেন্স টেবিল

উপরের বার্তাগুলো এবার একটা সম্পূর্ণ রেফারেন্স টেবিলে সংগঠিত করি — একটা cache-এর বর্তমান state, আর তার উপর ঘটা প্রতিটা সম্ভাব্য ঘটনার জন্য পরবর্তী state। “Local” মানে এই core নিজে সেই অপারেশন করছে; “Remote” মানে অন্য কোনো core-এর অনুরোধে snoop করে প্রতিক্রিয়া জানানো হচ্ছে।

বর্তমান stateLocal readLocal writeRemote read (অন্য core পড়তে চায়)Remote write (অন্য core লিখতে চায়)
Imiss → E বা S (নিচে দেখুন)miss → M (সবাইকে invalidate)কোনো প্রভাব নেই (এমনিতেই invalid)কোনো প্রভাব নেই
Elocal hit, state অপরিবর্তিতকোনো bus message ছাড়াই E→Mdata সরবরাহ (বা memory থেকে, দুইটাই বৈধ কারণ clean), E→Sনিজের কপি invalidate (data clean, memory-ই যথেষ্ট), E→I
Slocal hit, state অপরিবর্তিতinvalidate broadcast, S→Mdata সরবরাহ (ঐচ্ছিক, অন্য S কপিও পারে), S→Sনিজের কপি invalidate, S→I
Mlocal hit, state অপরিবর্তিতlocal hit, state অপরিবর্তিতdata সরবরাহ + writeback (MESI) বা O-তে যাওয়া (MOESI), M→S বা M→Odata সরবরাহ + writeback, M→I

আর read miss-এ E বনাম S-এর সিদ্ধান্ত: snoop-এ কেউ সাড়া না দিলে (কেউ cache করেনি) → E। কেউ সাড়া দিলে (অন্তত একজনের কাছে ইতিমধ্যে বৈধ কপি আছে) → উভয় পক্ষই (বা সবপক্ষ) S।

উদাহরণ

সম্পূর্ণ Worked Example — Read, Read, Write, আবার Read

এবার এই লেসনের কেন্দ্রীয় দাবিটা — একটা সম্পূর্ণ, ধাপে-ধাপে state-transition sequence, দুইটা (পরে তিনটা) core নিয়ে, একটা একক memory address X-এর জন্য। শুরুতে কেউই X cache করেনি, memory-তে X-এর মান V0V_0।

ধাপঘটনাBus-এ বার্তাCore A stateCore B stateCore C stateMemory-তে মান
০(শুরু, কেউ cache করেনি)—IIIV0V_0
১A পড়ে Xread request; কেউ সাড়া দেয়নি (কেউ cache করেনি)EIIV0V_0
২B পড়ে Xread request; A snoop করে, নিজের E কপি আছে দেখে সাড়া দেয়SSIV0V_0
৩A লেখে X-এinvalidate request (A-র ইতিমধ্যে S আছে, শুধু ownership দরকার); B snoop করে নিজের কপি invalidate করেMIIV0V_0 (এখন stale — cache-এ dirty ডেটা)
৪C পড়ে Xread request; A snoop করে, নিজের M কপি আছে দেখে data সরবরাহ করে + memory-তে writeback করেSISV1V_1 (writeback-এর ফলে আপডেট)

ধাপে ধাপে যুক্তি:

ধাপ ১ (A পড়ে): X কোথাও cache নেই। A একটা read request পাঠায়। কেউ সাড়া দেয় না (কারো কাছে কপি নেই), তাই memory থেকে সরাসরি ডেটা আসে। যেহেতু hardware নিশ্চিত কেউ এটা শেয়ার করছে না (কোনো snoop hit হয়নি), A সরাসরি E state-এ যায় — একমাত্র কপি, clean।

ধাপ ২ (B পড়ে): B-রও X দরকার, miss হয়, read request পাঠায়। এবার A snoop করে দেখে তার কাছে E state-এ কপি আছে — যেহেতু এখন দুইটা cache-এ কপি থাকবে, দুইটাকেই S-এ নামতে হয় (E আর “একমাত্র কপি” থাকতে পারে না)। A নিজের state E→S-এ নামায় (আর data B-কে সরবরাহ করে, অথবা memory থেকেও আসতে পারে যেহেতু clean — বাস্তবায়ন-নির্ভর, উভয়ই logically সঠিক)। B নতুন কপি S state-এ পায়।

ধাপ ৩ (A লেখে): A-র কাছে ইতিমধ্যে বৈধ (S) ডেটা আছে, শুধু লেখার অনুমতি দরকার — exclusive ownership। A একটা invalidate request পাঠায় (ডেটা আনার দরকার নেই, শুধু অন্য সবাইকে সরিয়ে দেওয়া)। B snoop করে দেখে তার কপি invalidate করতে হবে — B: S→I। A নিজে এখন একমাত্র বৈধ কপি, লেখে, state S→M (dirty, memory এখনো পুরনো V0V_0)।

ধাপ ৪ (C পড়ে): C-র miss হয়, read request পাঠায়। A snoop করে দেখে তার কাছে M state-এ (dirty) কপি আছে — A-কেই এখন ডেটা সরবরাহ করতে হবে (cache-to-cache transfer), এবং যেহেতু plain MESI-তে dirty ডেটা শেয়ার করার সাথে সাথে সেটা “clean” বলে গণ্য করতে হয় (আর কোনো একক owner থাকছে না), A লেখা মান memory-তে writeback করে দেয়। A নিজের state M→S-এ নামে, C নতুন কপি S state-এ পায়, memory এখন আপডেটেড (V1V_1)।

MOESI — Owned State, লেখা writeback ছাড়াই শেয়ার করা

ধাপ ৪-এ লক্ষ্য করুন — C পড়তে চাওয়ায় A-কে বাধ্যতামূলকভাবে memory-তে writeback করতে হলো, শুধু C-কে ডেটা দেওয়ার জন্য। যদি এরপর D-ও পড়তে চায়, memory থেকে (এখন সঠিক) ডেটা দেওয়া যাবে, কোনো সমস্যা নেই — কিন্তু সেই writeback-টা একটা বাস্তব memory-bandwidth খরচ ছিল, যেটা এড়ানো যেত যদি শুধু cache-to-cache transfer-এই যথেষ্ট হতো।

MOESI protocol (MESI + একটা পঞ্চম state, O = Owned) ঠিক এই অপচয় এড়ায়:

ধাপ (MOESI বিকল্প)ঘটনাCore A stateCore C stateMemory
৪′C পড়ে X; A snoop করে data সরবরাহ করে, কিন্তু writeback করে নাO (dirty, কিন্তু এখন শেয়ারিং দায়িত্বপ্রাপ্ত)SV0V_0 (এখনো stale!)

O (Owned)-এর অর্থ: “আমার কপি dirty (memory-র চেয়ে ভিন্ন), এবং অন্য cache-ও (S state-এ) এই একই মান শেয়ার করছে — কিন্তু memory-তে writeback করার দায়িত্ব আমারই, যখনই সেটা প্রয়োজন হবে (যেমন আমি নিজে evict হলে, বা কেউ লিখতে চাইলে)।” C-র কপি এখন S — clean বলেই গণ্য, যদিও memory আসলে stale — কারণ C জানে (protocol অনুযায়ী) যদি তার সঠিক মান দরকার হয়, A (Owner)-ই সত্যের উৎস, memory না।

MESI: dirty শেয়ার করতে হলে  ⟹  বাধ্যতামূলক immediate writeback\text{MESI: dirty শেয়ার করতে হলে} \implies \text{বাধ্যতামূলক immediate writeback} MOESI: dirty শেয়ার করা যায়  ⟹  writeback পরে, দরকার হলে, Owner দায়িত্ব নেয়\text{MOESI: dirty শেয়ার করা যায়} \implies \text{writeback পরে, দরকার হলে, Owner দায়িত্ব নেয়}

এই একটা state যোগ করে MOESI প্রতিটা “dirty ডেটা একাধিক reader-এর মধ্যে বারবার পাস হওয়া” পরিস্থিতিতে বাস্তব memory-bandwidth সাশ্রয় করে — বিশেষত producer-consumer প্যাটার্নের multi-threaded কোডে, যেখানে একটা core বারবার লেখে আর একাধিক core বারবার পড়ে।

নিজে চালিয়ে দেখুন

EXPERIMENT

Coherence traffic-এর real latency cost মাপুন — 'ping-pong' বেঞ্চমার্ক

Linux/macOS, ≥2 core, C বা Python (C-তে পার্থক্য অনেক তীক্ষ্ণ)· ২০ মিনিট

ধারণা: দুইটা thread, দুইটা ভিন্ন core-এ pin করা। একটাতে (A) দুইটাই একটা একক, shared counter বারবার increment করে (প্রতিবার cache line-টা A থেকে B-তে, B থেকে A-তে বারবার “পিং-পং” করবে — বারবার M→I, I→M)। আরেকটাতে (B) প্রতিটা thread তার নিজস্ব, আলাদা cache line-এ (অন্তত ৬৪ বাইট দূরত্বে) counter increment করে — কোনো coherence traffic লাগবে না।

#include \<pthread.h\>
#include \<stdio.h\>
#include \<time.h\>

#define ITERATIONS 50000000

volatile long shared_counter = 0;   /* দুই thread-ই একই line-এ লিখবে */

struct padded_counter {
    volatile long value;
    char padding[56];                /* বাকি cache line ভরাট — অন্য variable-এর সাথে line শেয়ার এড়াতে */
};
struct padded_counter counters[2];  /* প্রতিটা thread-এর নিজস্ব line */

void *shared_worker(void *arg) {
    for (long i = 0; i \< ITERATIONS; i++) shared_counter++;
    return NULL;
}

void *private_worker(void *arg) {
    int id = *(int *)arg;
    for (long i = 0; i \< ITERATIONS; i++) counters[id].value++;
    return NULL;
}

double run(void *(*fn)(void *), void *args[2]) {
    struct timespec t0, t1;
    pthread_t th[2];
    clock_gettime(CLOCK_MONOTONIC, &t0);
    pthread_create(&th[0], NULL, fn, args[0]);
    pthread_create(&th[1], NULL, fn, args[1]);
    pthread_join(th[0], NULL);
    pthread_join(th[1], NULL);
    clock_gettime(CLOCK_MONOTONIC, &t1);
    return (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1e9;
}

int main(void) {
    int ids[2] = {0, 1};
    void *no_args[2] = {NULL, NULL};
    void *id_args[2] = {&ids[0], &ids[1]};

    double t_shared = run(shared_worker, no_args);
    double t_private = run(private_worker, id_args);

    printf("shared line (real coherence traffic):  %.3f সেকেন্ড\n", t_shared);
    printf("private line (কোনো coherence traffic না): %.3f সেকেন্ড\n", t_private);
    printf("অনুপাত: %.2fx\n", t_shared / t_private);
    return 0;
}

প্রত্যাশিত ফলাফলের ধরন (মেশিনভেদে বদলাবে):

shared line (real coherence traffic):  4.820 সেকেন্ড
private line (কোনো coherence traffic না): 0.410 সেকেন্ড
অনুপাত: 11.76x

এই পার্থক্য প্রায় সম্পূর্ণভাবে coherence traffic-এর দাম — shared_counter++ প্রতিবার আগে read করে, তারপর লেখে; দুই thread পালাক্রমে লিখলে প্রতিটা লেখাই আগের thread-এর কপি invalidate করে, নিজের জন্য নতুন M state আনতে বাধ্য করে একটা bus/interconnect round-trip। private_worker-এ প্রতিটা thread তার নিজস্ব, প্যাডেড (padded) cache line-এ কাজ করে — কখনো invalidate হয় না, পুরোটাই local L1 hit-এর গতিতে চলে।

এটা কী প্রমাণ করে

একটা cache line-এ দুইটা core বারবার পালাক্রমে লিখলে (সত্যিকারের, ইচ্ছাকৃত sharing — false sharing না, পরের লেসনের বিষয়), প্রতিটা লেখাই M→I এবং I→M state-transition আর একটা real inter-core message ঘটায় — এই coherence traffic স্বাধীন, cache-local লেখার চেয়ে বহুগুণ ধীর।

নিজে বানান

BUILD IT

একটা MESI State Machine Simulator

Python · ●●●●○
  1. প্রতিটা core-এর জন্য একটা dict বানান address → state (ডিফল্ট Invalid)
  2. একটা shared "bus log" লিস্ট বানান, প্রতিটা coherence message (read request/invalidate/data supply) এখানে যোগ হবে
  3. একটা read(core, addr) ফাংশন লিখুন যা এই লেসনের ধাপ ১-২-এর যুক্তি বাস্তবায়ন করে — অন্য কারো কাছে কপি না থাকলে E, থাকলে সবাইকে S বানায়
  4. একটা write(core, addr) ফাংশন লিখুন যা ধাপ ৩-এর যুক্তি বাস্তবায়ন করে — অন্য সবার কপি invalidate করে, লেখক core-কে M বানায়
  5. এই লেসনের সম্পূর্ণ worked example (A পড়ে, B পড়ে, A লেখে, C পড়ে) চালিয়ে output-এর state-table এই লেসনের টেবিলের সাথে হুবহু মেলে কিনা যাচাই করুন
  6. মোট bus message সংখ্যা গণনা করুন আর তুলনা করুন — যদি A বারবার লিখতে থাকে (কোনো অন্য core না পড়েই), কত কম message লাগে (E→M কোনো bus transaction ছাড়াই) তার বিপরীতে A-B পালাক্রমে লিখলে
class MESICache:
    def __init__(self, name):
        self.name = name
        self.states = {}  # addr -> 'M' | 'E' | 'S' | 'I'

    def state(self, addr):
        return self.states.get(addr, 'I')

    def set_state(self, addr, s):
        self.states[addr] = s


class System:
    def __init__(self, core_names):
        self.caches = {n: MESICache(n) for n in core_names}
        self.memory = {}
        self.bus_log = []

    def read(self, core, addr):
        c = self.caches[core]
        if c.state(addr) != 'I':
            self.bus_log.append(f"{core}: local hit ({c.state(addr)}), কোনো bus message লাগেনি")
            return

        sharers = [n for n, other in self.caches.items()
                   if n != core and other.state(addr) in ('M', 'E', 'S')]
        self.bus_log.append(f"{core}: BusRd({addr}) broadcast")

        if not sharers:
            c.set_state(addr, 'E')
            self.bus_log.append(f"  → কেউ সাড়া দেয়নি, memory থেকে ডেটা, {core}: E")
            return

        for s in sharers:
            other = self.caches[s]
            if other.state(addr) == 'M':
                self.memory[addr] = "writeback হয়েছে (M থেকে)"
                self.bus_log.append(f"  → {s} snoop করল (M), data supply + writeback, {s}: M→S")
                other.set_state(addr, 'S')
            elif other.state(addr) == 'E':
                self.bus_log.append(f"  → {s} snoop করল (E), data supply, {s}: E→S")
                other.set_state(addr, 'S')
            else:
                self.bus_log.append(f"  → {s} snoop করল (S), ইতিমধ্যে shared")
        c.set_state(addr, 'S')
        self.bus_log.append(f"  → {core}: S")

    def write(self, core, addr):
        c = self.caches[core]
        state = c.state(addr)

        if state == 'M':
            self.bus_log.append(f"{core}: local write, ইতিমধ্যে M, কোনো bus message লাগেনি")
            return
        if state == 'E':
            c.set_state(addr, 'M')
            self.bus_log.append(f"{core}: E→M, কোনো bus message লাগেনি (ইতিমধ্যে একমাত্র কপি)")
            return

        others = [n for n, other in self.caches.items() if n != core and other.state(addr) != 'I']
        self.bus_log.append(f"{core}: Invalidate({addr}) broadcast")
        for o in others:
            self.bus_log.append(f"  → {o} snoop করল, invalidate: {self.caches[o].state(addr)}→I")
            self.caches[o].set_state(addr, 'I')
        c.set_state(addr, 'M')
        self.bus_log.append(f"  → {core}: {state}→M")


sys_ = System(['A', 'B', 'C'])
sys_.read('A', 'X')   # প্রত্যাশিত: A অ্যাডভান্স E
sys_.read('B', 'X')   # প্রত্যাশিত: A,B উভয়ই S
sys_.write('A', 'X')  # প্রত্যাশিত: A: M, B: I
sys_.read('C', 'X')   # প্রত্যাশিত: A: S (writeback-সহ), C: S

for line in sys_.bus_log:
    print(line)

print("\nচূড়ান্ত states:", {n: c.state('X') for n, c in sys_.caches.items()})

প্রত্যাশিত final state: {'A': 'S', 'B': 'I', 'C': 'S'} — ঠিক এই লেসনের টেবিলের ধাপ ৪-এর সাথে মিলবে। এই simulator চালিয়ে নিজে যাচাই করুন misconception অংশের “সাধারণ ভুল” গুলো (A সরাসরি I-তে যাওয়া, ইত্যাদি) simulator-এ ঘটছে না।

বাস্তব সিস্টেমে

Cache coherence বাস্তব সিস্টেমে

  • Intel/AMD x86-64 — আধুনিক সব multi-core x86 CPU হার্ডওয়্যার cache coherence প্রয়োগ করে (একে বলা হয় “cache coherent”, সংক্ষেপে ccNUMA বড় সিস্টেমে)। Intel প্রায়ই MESIF ব্যবহার করে — MESI + একটা “Forward” state, যা ঠিক করে দেয় একাধিক S-state কপির মধ্যে কোনটা নতুন reader-কে ডেটা সরবরাহ করবে (শুধু memory-ই না, কোনো একটা নির্দিষ্ট cache-ও দায়িত্ব নেয়, বাড়তি সমন্বয়-দায়িত্ব একটা কপির উপর কেন্দ্রীভূত করে)।

  • AMD (Opteron থেকে Zen পর্যন্ত) — MOESI ব্যবহার করে, এই লেসনের O state ঠিক এই vendor-এর বাস্তব প্রয়োগ। বিশেষত multi-socket সার্ভার ডিজাইনে (একাধিক চিপ, Infinity Fabric দিয়ে যুক্ত), dirty ডেটা cache-to-cache শেয়ার করে বারবার memory writeback এড়ানো একটা বাস্তব bandwidth-সাশ্রয়ী কৌশল।

  • ARM ইন্টারকানেক্ট (CCI, CMN, AMBA ACE) — ARM-এর multi-core ডিজাইনে coherence প্রায়ই MOESI-ধাঁচের একটা প্রোটোকল অনুসরণ করে, ARM-এর নিজস্ব coherent interconnect fabric (CoreLink CMN সিরিজ) দিয়ে বাস্তবায়িত — phone/tablet SoC-তে CPU cluster-দের মধ্যে coherence বজায় রাখতে ব্যবহৃত।

  • Multi-socket সার্ভার (QPI/UPI, Infinity Fabric) — একাধিক physical চিপ (socket) যখন একসাথে coherent memory শেয়ার করে, broadcast snooping আর ব্যবহারিক থাকে না (latency আর bandwidth-এর কারণে) — এখানে directory-based coherence বাধ্যতামূলক। এটাই এই module-এর পরবর্তী একটা topic (NUMA topology) সরাসরি প্রাসঙ্গিক করে তোলে — coherence থাকলেও, দূরত্বভেদে latency ভিন্ন হতে পারে।

  • perf c2c (cache-to-cache) — Linux-এর বাস্তব coherence-debugging টুল — Linux perf c2c কমান্ড সরাসরি hardware performance counter ব্যবহার করে detect করে কোন cache line-এ “HITM” (Hit Modified — অন্য core-এর M-state কপি থেকে data সরবরাহ) ঘটছে বেশি, অর্থাৎ ঠিক কোথায় এই লেসনের coherence traffic বাস্তব প্রোগ্রামে ঘটছে সবচেয়ে বেশি — এই লেসনের ধারণাগুলোর সরাসরি, দৈনন্দিন প্রকৌশলিক প্রয়োগ।

  • False sharing — coherence-এর দাম যখন বাগে পরিণত হয় — পরের লেসনের (false-sharing) মূল বিষয়: যখন দুইটা সম্পূর্ণ অসম্পর্কিত variable কাকতালীয়ভাবে একই ৬৪-বাইট cache line-এ পড়ে, দুইটা ভিন্ন core সেগুলোতে স্বাধীনভাবে লিখলেও, hardware-এর দৃষ্টিতে তারা একই line-এ লিখছে — এই লেসনের experiment-এর shared_counter উদাহরণের মতোই ping-pong ঘটে, যদিও প্রোগ্রামার কোনো logical sharing চায়নি।

  • GPU-CPU কোহেরেন্স — সবসময় ছিল না, এখন আসছে — পুরনো GPU ডিজাইনে CPU আর GPU memory সম্পূর্ণ আলাদা ছিল, explicit copy (যেমন CUDA-র cudaMemcpy) ছাড়া কোনো coherence গ্যারান্টিই ছিল না। আধুনিক ডিজাইন (Apple-এর unified memory architecture, NVIDIA-র NVLink-C2C, AMD-র Infinity Cache-ভিত্তিক APU) ধীরে ধীরে CPU-GPU-এর মধ্যেও hardware coherence আনছে — প্রমাণ করে coherence কোনো “সবসময়-থাকা” প্রাকৃতিক বৈশিষ্ট্য না, এটা একটা স্পষ্ট, দামি প্রকৌশল সিদ্ধান্ত যেখানে প্রয়োজন সেখানেই যোগ করা হয়।

যে ভুলগুলো সবাই করে

“Cache coherence মানে সব core-এর cache সবসময় বাস্তব-সময়ে (real-time) সিঙ্ক্রোনাইজড, একটা ক্রমাগত broadcast চলতে থাকে।”

না — coherence একটা on-demand গ্যারান্টি, ক্রমাগত ব্যাকগ্রাউন্ড সিঙ্ক্রোনাইজেশন না। যতক্ষণ কোনো core একটা address-এ access করছে না, তার cache সেই address সম্পর্কে যা খুশি (এমনকি stale) অবস্থায় “চুপচাপ” বসে থাকতে পারে — protocol শুধু তখনই সক্রিয় হয় যখন একটা conflicting access ঘটে (একটা core পড়তে/লিখতে চায়, আর অন্য কারো কাছে সংশ্লিষ্ট কপি থাকে)। এই লেসনের worked example-এর প্রতিটা ধাপ একটা নির্দিষ্ট read/write ঘটনার প্রতিক্রিয়া, কোনো ঘড়ি-চালিত polling না।

“Invalid (I) state মানে সেই cache-এ সেই address-এর জন্য কোনো ডেটা physically নেই।”

cache-organization লেসনের valid bit আলোচনার সরাসরি প্রতিধ্বনি — I state মানে শুধু এটাই যে হার্ডওয়্যার সেই line-এর content-কে ব্যবহারযোগ্য বলে গণ্য করবে না, hit-check-এ সবসময় miss হিসেবে ধরবে। Physically, সেই SRAM cell-গুলোতে পুরনো bit pattern এখনও বসে থাকতে পারে, নতুন কিছু দিয়ে ওভাররাইট না হওয়া পর্যন্ত — শুধু state metadata (tag-এর পাশের ছোট state field) বলে দেয় এই content বিশ্বাসযোগ্য না। এটা ঠিক direct-mapped cache-এর valid bit-এর ধারণার সম্প্রসারণ, এখন চার-মানের একটা state field হিসেবে।

“Modified (M) state-এ থাকা একটা লেখা সরাসরি memory-তে যায় — তাই M state-এর লেখা 'ধীর'।”

উল্টো — M state-এর পুরো পয়েন্টই হলো লেখাটা শুধু local cache-এ ঘটে, কোনো memory access ছাড়াই (এই কারণেই write-back নীতি, cache-policies লেসনের বিষয়, দ্রুত)। যা ধীর, তা হলো আগের ধাপ — E বা S থেকে M-এ পৌঁছাতে অন্য cache-দের invalidate করার bus transaction (যদি লাগে)। একবার M state-এ পৌঁছালে, পরবর্তী লেখাগুলো (যতক্ষণ অন্য কেউ interfere না করে) সম্পূর্ণ local-gতির, cache-organization-এর সাধারণ cache hit-এর মতোই দ্রুত।

“Cache coherence protocol নিশ্চিত করে multi-threaded প্রোগ্রামে data race বা logical bug হবে না।”

এটা এই লেসনের সবচেয়ে গুরুত্বপূর্ণ, সূক্ষ্ম সংশোধন — coherence আর program correctness সম্পূর্ণ ভিন্ন স্তরের বিষয়। MESI নিশ্চিত করে একটা মাত্র address-এর সব cache কপি শেষ পর্যন্ত সামঞ্জস্যপূর্ণ থাকবে (write propagation + serialization, concept অংশের সংজ্ঞা)। কিন্তু এটা কিছুই বলে না ভিন্ন ভিন্ন address-এর মধ্যে অপারেশনের আপেক্ষিক ক্রম নিয়ে — একটা thread যদি data = 42 লিখে তারপর ready = true লিখে, coherence একাই গ্যারান্টি দেয় না যে অন্য thread ready-কে true দেখার পরে data-কেও আপডেটেড দেখবে (compiler/hardware reordering-এর কারণে)। এটা memory consistency model-এর প্রশ্ন, আর সঠিক multi-threaded synchronization-এর জন্য প্রয়োজন হয় explicit atomics/memory barrier (এই module-এর পরের একটা লেসনের বিষয়), শুধু coherence-ই যথেষ্ট না।

বুঝেছেন কি না দেখুন

1

MESI-এর চারটা state-এর নাম আর প্রতিটার এক-লাইনের অর্থ বলুন।

স্মরণ

M (Modified) — একমাত্র কপি, dirty (memory-র থেকে ভিন্ন)। E (Exclusive) — একমাত্র কপি, clean (memory-র সাথে অভিন্ন)। S (Shared) — একাধিক cache-এ থাকতে পারা কপি, সব clean, memory-র সাথে অভিন্ন। I (Invalid) — কপি stale/অকেজো, hit হিসেবে গণ্য হয় না।

2

তিনটা core — P, Q, R। শুরুতে কেউই address Y cache করেনি। ক্রমানুসারে ঘটে: (১) Q পড়ে Y, (২) R পড়ে Y, (৩) Q লেখে Y-এ। প্রতিটা ধাপের পরে P, Q, R-এর state কী?

প্রয়োগ

ধাপ ১ (Q পড়ে): কেউ cache করেনি, তাই কোনো snoop hit নেই। Q: E। P, R: I (অপরিবর্তিত)।

ধাপ ২ (R পড়ে): R miss করে, snoop করে দেখে Q-র কাছে E কপি আছে। যেহেতু এখন দুইটা কপি হবে, দুইটাই S-এ নামে। Q: E→S। R: S। P: I (কখনো জড়ায়নি)।

ধাপ ৩ (Q লেখে): Q-র কাছে ইতিমধ্যে S আছে, শুধু ownership দরকার — invalidate broadcast পাঠায়। R snoop করে নিজের কপি invalidate করে: R: S→I। Q: S→M। P: I (অপরিবর্তিত, কখনো জড়ায়নি বলে কোনো প্রভাব নেই)।

চূড়ান্ত অবস্থা: P: I, Q: M, R: I।

3

MESI-তে E state না থাকলে (অর্থাৎ শুধু MSI হলে) কী পার্থক্য হতো একটা single-threaded প্রোগ্রামে যেখানে একটা core একটা variable প্রথমে read করে (initialize করার জন্য) তারপর সাথে সাথেই write করে, আর অন্য কোনো core কখনোই সেই variable ছুঁয়ে দেখে না?

যুক্তি

MESI-তে: read miss-এ কোনো snoop hit হয় না (কেউ cache করেনি), তাই সরাসরি E state পাওয়া যায়। পরের write-এ E→M transition-এ কোনো bus transaction লাগে না — hardware ইতিমধ্যে জানে এটাই একমাত্র কপি।

শুধু MSI (E ছাড়া) হলে: read miss-এ সরাসরি S state-এ যেতে হতো (কারণ MSI-তে “একমাত্র কপি” বলার কোনো state নেই, সবসময় সতর্কতাবশত S ধরে নেওয়া হয়)। পরের write-এ S→M transition-এর জন্য একটা invalidate broadcast পাঠাতে হতো, যদিও বাস্তবে কোনো অন্য core-এর কাছে কপিই ছিল না — একটা সম্পূর্ণ অপ্রয়োজনীয় bus transaction, শুধু protocol-এর সীমাবদ্ধতার কারণে।

তাই এই সাধারণ, খুবই প্রচলিত প্যাটার্নে (read-then-write, single-threaded বা একা ব্যবহৃত ডেটা) MESI-এর E state একটা বাস্তব, পরিমাপযোগ্য coherence-ট্র্যাফিক সাশ্রয় দেয় MSI-এর তুলনায়।

4

দুইটা core, A আর B, একটা shared cache line-এ পালাক্রমে বারবার লিখতে থাকে (A লেখে, B লেখে, A লেখে, …)। প্রতিটা লেখার আগে ঠিক কী coherence ঘটনা ঘটবে, আর এই প্যাটার্নটা কেন এই module-এর পরের লেসনের (false sharing) সাথে সরাসরি সম্পর্কিত?

যুক্তি

প্রতিটা লেখার আগে: লেখক core-এর কাছে যদি কপি না থাকে (আগের লেখক-এর invalidate-এর কারণে থাকবে না), তাকে একটা read-for-ownership-ধরনের request পাঠাতে হয় — অন্য core (যার কাছে M state-এ কপি আছে) snoop করে data সরবরাহ করে এবং নিজের কপি invalidate করে (M→I), নতুন লেখক M state পায়। পরের লেখার আগে ঠিক উল্টোটা ঘটে। অর্থাৎ প্রতিটা লেখা একটা সম্পূর্ণ inter-core round-trip ঘটায় — কখনোই স্থিতিশীল M state-এ থেকে দ্রুত local লেখা পাওয়া যায় না, কারণ line-টা প্রতিবার “পিং-পং” করছে।

এটা ঠিক এই লেসনের experiment-এর shared_counter পরিস্থিতি — এখানে sharing ইচ্ছাকৃত (সত্যিকারের shared variable)। পরের লেসনের (false-sharing) মূল বিন্দু: এই ঠিক একই ব্যয়বহুল ping-pong ঘটতে পারে এমনকি যখন A আর B সম্পূর্ণ আলাদা, অসম্পর্কিত variable-এ লিখছে — শুধু কাকতালীয়ভাবে সেই দুইটা variable একই ৬৪-বাইট cache line-এ memory-তে পাশাপাশি বসে যাওয়ায়। Coherence protocol line-এর ভেতরের কোন বাইট বদলেছে তা জানে না/পরোয়া করে না — পুরো line-কে একক এককভাবে দেখে, তাই “ভিন্ন ডেটা” হলেও protocol একই আচরণ করে।

5

একটা producer-consumer প্যাটার্নে, core P বারবার একটা shared buffer-এ লেখে, আর core Q১, Q২, Q৩ — তিনটা core — বারবার সেই একই buffer পড়ে (কিন্তু কখনো লেখে না)। এই পরিস্থিতিতে MESI বনাম MOESI-এর মধ্যে বাস্তব পারফরম্যান্স পার্থক্য কী হবে, আর কেন?

ডিজাইন

প্রতিবার P লেখে, তার কপি M state-এ যায় (অন্য সবাইকে invalidate করে)। এরপর Q১, Q২, Q৩ পালাক্রমে পড়তে চাইলে:

MESI-তে: প্রথম reader (ধরুন Q১) পড়তে চাইলে, P-কে (M state-এ) data সরবরাহ এবং memory-তে writeback করতে হয় (ধাপ ৪-এর যুক্তি), P: M→S, Q১: S। এরপর Q২ পড়তে চাইলে, এবার P বা Q১ (উভয়ই S, clean) থেকে বা memory থেকে সহজেই ডেটা পাওয়া যায় — কোনো নতুন writeback লাগে না, কারণ ইতিমধ্যেই clean।

MOESI-তে: প্রথম reader Q১ পড়তে চাইলে, P data সরবরাহ করে কিন্তু writeback করে না — P: M→O, Q১: S। মূল পার্থক্য: MOESI-তে এই memory writeback-টা এড়ানো গেল। যেহেতু P বারবার নতুন করে লিখতে থাকবে (producer প্যাটার্ন), প্রতিটা লেখার চক্রে MESI একটা করে বাধ্যতামূলক memory writeback করবে (প্রথম reader আসার সময়), যেখানে MOESI সেই writeback সম্পূর্ণ এড়িয়ে শুধু cache-to-cache transfer-এই কাজ চালাবে — যতক্ষণ না P নিজে evict হয় বা memory-access সত্যিই আবশ্যক হয়।

ফলাফল: ঘন ঘন producer-write, বহু-reader প্যাটার্নে MOESI বাস্তবে কম memory bandwidth ব্যবহার করে MESI-এর তুলনায় — ঠিক এই কারণেই AMD-এর মতো vendor যাদের workload-এ এই প্যাটার্ন সাধারণ (multi-threaded সার্ভার/HPC কোড), MOESI বেছে নিয়েছে।

এরপর কী

আমরা এখন সম্পূর্ণভাবে দেখেছি কীভাবে multi-core CPU নিশ্চিত করে সব core একই memory-র একটা সামঞ্জস্যপূর্ণ ছবি দেখে — MESI-এর চার state, bus snooping, আর MOESI-এর memory-bandwidth সাশ্রয়। কিন্তু এই লেসনের experiment আর check-এর প্রশ্ন ৪ ইচ্ছাকৃতভাবে একটা ভয়ংকর বাস্তবতার দিকে ইঙ্গিত করেছে: coherence protocol পুরো cache line-কে একক এককভাবে দেখে, ভেতরের আলাদা variable-কে না।

এর মানে দুইটা প্রোগ্রামার-দৃষ্টিতে সম্পূর্ণ স্বাধীন variable, যাদের মধ্যে কোনো logical sharing নেই, শুধু memory layout-এর কাকতালে একই ৬৪-বাইট line-এ বসে গেলে ঠিক এই লেসনের ব্যয়বহুল ping-pong ঘটাতে পারে — সম্পূর্ণ অদৃশ্যভাবে, প্রোগ্রামের logic-এ কোনো bug ছাড়াই, শুধু পারফরম্যান্সে। পরের লেসনে (false-sharing) আমরা ঠিক এই সমস্যাটা — কীভাবে চিনতে হয়, কীভাবে মাপতে হয়, আর কীভাবে (padding, alignment দিয়ে) এড়াতে হয় — বিস্তারিত দেখব।

আরও পড়ুন

  • Computer Architecture: A Quantitative Approach, Chapter 5 — Hennessy & Patterson · MESI/MOESI ও snooping/directory coherence-এর প্রামাণ্য আলোচনা
  • A Primer on Memory Consistency and Cache Coherence — Sorin, Hill, Wood · Coherence ও consistency-র পার্থক্যসহ সম্পূর্ণ প্রযুক্তিগত ভিত্তি
  • AMD64 Architecture Programmer's Manual, Volume 2 — System Programming — AMD · MOESI protocol-এর বাস্তব বাস্তবায়নের বিবরণ